@themoltnet/agent-daemon 0.16.0 → 0.18.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 +24 -8
  2. package/dist/main.js +908 -243
  3. package/package.json +9 -8
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
@@ -4273,18 +4273,120 @@ var TaskContext = _Array_(_Object_({
4273
4273
  maxItems: 5
4274
4274
  });
4275
4275
  //#endregion
4276
- //#region ../../libs/tasks/src/daemon-profiles.ts
4277
- var DaemonProfileName = String$1({
4276
+ //#region ../../libs/tasks/src/rubric.ts
4277
+ /**
4278
+ * Rubric — structured acceptance criteria used by judgment tasks.
4279
+ *
4280
+ * Phase 1 (this PR): rubrics are embedded in task inputs. Their integrity
4281
+ * is pinned via the task's `input_cid` (which covers the whole input,
4282
+ * including the inline rubric). No separate storage, no CRUD.
4283
+ *
4284
+ * Phase 2 (see #881): rubrics become a first-class resource with their
4285
+ * own signed rows and CIDv1 lookup. The schema below is designed to
4286
+ * carry forward unchanged — only storage and addressing differ.
4287
+ *
4288
+ * Until Phase 2 lands, `rubricId` + `version` + `contentHash` are
4289
+ * informational fields the author fills in; no uniqueness is enforced.
4290
+ * `contentHash` is optional in Phase 1 because the *task*'s input_cid
4291
+ * is the authoritative commitment.
4292
+ */
4293
+ /**
4294
+ * How a judge must score a single criterion.
4295
+ *
4296
+ * - `llm_score`: 0..1 continuous, `rationale` required. Smooths failures
4297
+ * into the gradient — use `llm_checklist` instead for properties where
4298
+ * a single failure is a real failure (grounding, faithfulness).
4299
+ * - `llm_checklist`: judge enumerates per-claim assertions with
4300
+ * `{passed, evidence}`. The criterion's numeric `score` is derived:
4301
+ * `1` iff every assertion passes, else `0`. Per-claim evidence is the
4302
+ * dataset for cluster-analysis of failure modes. See #999.
4303
+ * - `boolean`: 0 or 1, `rationale` optional.
4304
+ * - `deterministic_signature_check`: judge runs a signature check;
4305
+ * result is 0 or 1. No LLM discretion.
4306
+ * - `deterministic_coverage_check`: every referenced source entry
4307
+ * appears in the rendered output; 0 or 1.
4308
+ */
4309
+ var RubricScoringMode = Union([
4310
+ Literal("llm_score"),
4311
+ Literal("llm_checklist"),
4312
+ Literal("boolean"),
4313
+ Literal("deterministic_signature_check"),
4314
+ Literal("deterministic_coverage_check")
4315
+ ], { $id: "RubricScoringMode" });
4316
+ /**
4317
+ * One binary check produced by an `llm_checklist`-mode criterion.
4318
+ *
4319
+ * `evidence` is REQUIRED for both PASS and FAIL — agentskills.io grading
4320
+ * principle: \"Don't give the benefit of the doubt.\" A PASS without
4321
+ * concrete evidence (a quoted span, an entry id, a source location)
4322
+ * cannot be audited. A FAIL without evidence cannot be clustered into
4323
+ * structural fixes. The same shape is reused by `judge-eval-variant`
4324
+ * (#943) so tooling, dashboards, and analysis stay uniform.
4325
+ */
4326
+ var AssertionResult = _Object_({
4327
+ id: String$1({ minLength: 1 }),
4328
+ text: String$1({ minLength: 1 }),
4329
+ passed: Boolean$1(),
4330
+ evidence: String$1({ minLength: 1 })
4331
+ }, {
4332
+ $id: "AssertionResult",
4333
+ additionalProperties: false
4334
+ });
4335
+ var RubricCriterion = _Object_({
4336
+ id: String$1({ minLength: 1 }),
4337
+ description: String$1({ minLength: 1 }),
4338
+ weight: Number$1({
4339
+ minimum: 0,
4340
+ maximum: 1
4341
+ }),
4342
+ scoring: RubricScoringMode
4343
+ }, {
4344
+ $id: "RubricCriterion",
4345
+ additionalProperties: false
4346
+ });
4347
+ /**
4348
+ * A complete rubric. Same shape used in Phase 1 (inline) and Phase 2
4349
+ * (stored row `body`); only the addressing mechanism differs.
4350
+ */
4351
+ var Rubric = _Object_({
4352
+ rubricId: String$1({ minLength: 1 }),
4353
+ version: String$1({ minLength: 1 }),
4354
+ preamble: Optional(String$1()),
4355
+ criteria: _Array_(RubricCriterion, { minItems: 1 }),
4356
+ scope: Optional(String$1()),
4357
+ contentHash: Optional(String$1())
4358
+ }, {
4359
+ $id: "Rubric",
4360
+ additionalProperties: false
4361
+ });
4362
+ /**
4363
+ * Verify rubric criteria weights sum to 1.0 within floating-point tolerance.
4364
+ * The schema constrains each weight to [0,1] but can't express a cross-field
4365
+ * sum constraint, so this is enforced programmatically by callers that
4366
+ * accept rubrics (task input validators, server-side task creation).
4367
+ *
4368
+ * Returns null when valid; otherwise an error message suitable for surfacing
4369
+ * to the caller. Tolerance is 1e-6 to accommodate JSON round-tripping of
4370
+ * decimal fractions (e.g. 0.1 + 0.2 + 0.3 + 0.4 ≠ 1.0 exactly).
4371
+ */
4372
+ function validateRubricWeights(rubric) {
4373
+ const sum = rubric.criteria.reduce((acc, c) => acc + c.weight, 0);
4374
+ if (Math.abs(sum - 1) > 1e-6) return `Rubric weights must sum to 1.0 (got ${sum.toFixed(6)})`;
4375
+ return null;
4376
+ }
4377
+ //#endregion
4378
+ //#region ../../libs/tasks/src/runtime-profiles.ts
4379
+ var RuntimeProfileName = String$1({
4278
4380
  minLength: 1,
4279
4381
  maxLength: 100,
4280
4382
  pattern: "^[a-zA-Z0-9][a-zA-Z0-9_-]{0,99}$"
4281
4383
  });
4282
- var DaemonProfileEnvName = String$1({
4384
+ var RuntimeProfileEnvName = String$1({
4283
4385
  minLength: 1,
4284
4386
  maxLength: 128,
4285
4387
  pattern: "^[A-Z_][A-Z0-9_]*$"
4286
4388
  });
4287
- var DaemonProfileToolName = String$1({
4389
+ var RuntimeProfileToolName = String$1({
4288
4390
  minLength: 1,
4289
4391
  maxLength: 128,
4290
4392
  pattern: "^[a-zA-Z0-9._/-]+$"
@@ -4297,7 +4399,7 @@ var SandboxResumeCommandWhenSchema = _Object_({ workspaceMode: Optional(_Array_(
4297
4399
  minItems: 1,
4298
4400
  maxItems: 3
4299
4401
  })) }, { additionalProperties: false });
4300
- var DaemonProfileSandboxResumeCommand = Union([String$1({
4402
+ var RuntimeProfileSandboxResumeCommand = Union([String$1({
4301
4403
  minLength: 1,
4302
4404
  maxLength: 4096
4303
4405
  }), _Object_({
@@ -4315,7 +4417,7 @@ var DaemonProfileSandboxResumeCommand = Union([String$1({
4315
4417
  maximum: 6e4
4316
4418
  }))
4317
4419
  }, { additionalProperties: false })]);
4318
- var DaemonProfileSandbox = _Object_({
4420
+ var RuntimeProfileSandbox = _Object_({
4319
4421
  snapshot: Optional(_Object_({
4320
4422
  setupCommands: Optional(_Array_(String$1({
4321
4423
  minLength: 1,
@@ -4331,7 +4433,7 @@ var DaemonProfileSandbox = _Object_({
4331
4433
  pattern: "^[0-9]+[KMGTP]?$"
4332
4434
  }))
4333
4435
  }, { additionalProperties: false })),
4334
- resumeCommands: Optional(_Array_(DaemonProfileSandboxResumeCommand, { maxItems: 30 })),
4436
+ resumeCommands: Optional(_Array_(RuntimeProfileSandboxResumeCommand, { maxItems: 30 })),
4335
4437
  vfs: Optional(_Object_({
4336
4438
  shadow: Optional(_Array_(String$1({
4337
4439
  minLength: 1,
@@ -4339,7 +4441,7 @@ var DaemonProfileSandbox = _Object_({
4339
4441
  }), { maxItems: 100 })),
4340
4442
  shadowMode: Optional(Union([Literal("deny"), Literal("tmpfs")]))
4341
4443
  }, { additionalProperties: false })),
4342
- env: Optional(Record(DaemonProfileEnvName, String$1({ maxLength: 4096 }))),
4444
+ env: Optional(Record(RuntimeProfileEnvName, String$1({ maxLength: 4096 }))),
4343
4445
  hostExec: Optional(_Object_({ autoApprove: Optional(Literal(false)) }, { additionalProperties: false })),
4344
4446
  resources: Optional(_Object_({
4345
4447
  memory: Optional(String$1({
@@ -4353,10 +4455,10 @@ var DaemonProfileSandbox = _Object_({
4353
4455
  }))
4354
4456
  }, { additionalProperties: false }))
4355
4457
  }, {
4356
- $id: "DaemonProfileSandbox",
4458
+ $id: "RuntimeProfileSandbox",
4357
4459
  additionalProperties: false
4358
4460
  });
4359
- var DaemonProfileContext = _Object_({
4461
+ var RuntimeProfileContext = _Object_({
4360
4462
  slug: String$1({
4361
4463
  minLength: 1,
4362
4464
  maxLength: 64,
@@ -4373,17 +4475,29 @@ var DaemonProfileContext = _Object_({
4373
4475
  maxLength: 65536
4374
4476
  })
4375
4477
  }, {
4376
- $id: "DaemonProfileContext",
4478
+ $id: "RuntimeProfileContext",
4377
4479
  additionalProperties: false
4378
4480
  });
4379
- var DaemonProfileRef = _Object_({ profileId: String$1({ format: "uuid" }) }, {
4380
- $id: "DaemonProfileRef",
4481
+ var RuntimeProfileRef = _Object_({ profileId: String$1({ format: "uuid" }) }, {
4482
+ $id: "RuntimeProfileRef",
4381
4483
  additionalProperties: false
4382
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
+ });
4383
4497
  _Object_({
4384
4498
  id: String$1({ format: "uuid" }),
4385
4499
  teamId: String$1({ format: "uuid" }),
4386
- name: DaemonProfileName,
4500
+ name: RuntimeProfileName,
4387
4501
  description: Union([String$1({ maxLength: 4096 }), Null()]),
4388
4502
  provider: String$1({
4389
4503
  minLength: 1,
@@ -4394,7 +4508,7 @@ _Object_({
4394
4508
  maxLength: 200
4395
4509
  }),
4396
4510
  runtimeKind: Literal("gondolin_pi"),
4397
- sandbox: DaemonProfileSandbox,
4511
+ sandbox: RuntimeProfileSandbox,
4398
4512
  sessionStorageMode: Literal("local"),
4399
4513
  workspaceStorageMode: Literal("local"),
4400
4514
  sessionTtlSec: Integer({
@@ -4405,9 +4519,12 @@ _Object_({
4405
4519
  minimum: 1,
4406
4520
  maximum: 86400
4407
4521
  }),
4408
- requiredEnv: _Array_(DaemonProfileEnvName, { maxItems: 100 }),
4409
- requiredTools: _Array_(DaemonProfileToolName, { maxItems: 100 }),
4410
- context: _Array_(DaemonProfileContext, { maxItems: 5 }),
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 }),
4411
4528
  revision: Integer({ minimum: 1 }),
4412
4529
  definitionCid: String$1({
4413
4530
  minLength: 1,
@@ -4418,112 +4535,10 @@ _Object_({
4418
4535
  createdAt: String$1({ format: "date-time" }),
4419
4536
  updatedAt: String$1({ format: "date-time" })
4420
4537
  }, {
4421
- $id: "DaemonProfile",
4538
+ $id: "RuntimeProfile",
4422
4539
  additionalProperties: false
4423
4540
  });
4424
4541
  //#endregion
4425
- //#region ../../libs/tasks/src/rubric.ts
4426
- /**
4427
- * Rubric — structured acceptance criteria used by judgment tasks.
4428
- *
4429
- * Phase 1 (this PR): rubrics are embedded in task inputs. Their integrity
4430
- * is pinned via the task's `input_cid` (which covers the whole input,
4431
- * including the inline rubric). No separate storage, no CRUD.
4432
- *
4433
- * Phase 2 (see #881): rubrics become a first-class resource with their
4434
- * own signed rows and CIDv1 lookup. The schema below is designed to
4435
- * carry forward unchanged — only storage and addressing differ.
4436
- *
4437
- * Until Phase 2 lands, `rubricId` + `version` + `contentHash` are
4438
- * informational fields the author fills in; no uniqueness is enforced.
4439
- * `contentHash` is optional in Phase 1 because the *task*'s input_cid
4440
- * is the authoritative commitment.
4441
- */
4442
- /**
4443
- * How a judge must score a single criterion.
4444
- *
4445
- * - `llm_score`: 0..1 continuous, `rationale` required. Smooths failures
4446
- * into the gradient — use `llm_checklist` instead for properties where
4447
- * a single failure is a real failure (grounding, faithfulness).
4448
- * - `llm_checklist`: judge enumerates per-claim assertions with
4449
- * `{passed, evidence}`. The criterion's numeric `score` is derived:
4450
- * `1` iff every assertion passes, else `0`. Per-claim evidence is the
4451
- * dataset for cluster-analysis of failure modes. See #999.
4452
- * - `boolean`: 0 or 1, `rationale` optional.
4453
- * - `deterministic_signature_check`: judge runs a signature check;
4454
- * result is 0 or 1. No LLM discretion.
4455
- * - `deterministic_coverage_check`: every referenced source entry
4456
- * appears in the rendered output; 0 or 1.
4457
- */
4458
- var RubricScoringMode = Union([
4459
- Literal("llm_score"),
4460
- Literal("llm_checklist"),
4461
- Literal("boolean"),
4462
- Literal("deterministic_signature_check"),
4463
- Literal("deterministic_coverage_check")
4464
- ], { $id: "RubricScoringMode" });
4465
- /**
4466
- * One binary check produced by an `llm_checklist`-mode criterion.
4467
- *
4468
- * `evidence` is REQUIRED for both PASS and FAIL — agentskills.io grading
4469
- * principle: \"Don't give the benefit of the doubt.\" A PASS without
4470
- * concrete evidence (a quoted span, an entry id, a source location)
4471
- * cannot be audited. A FAIL without evidence cannot be clustered into
4472
- * structural fixes. The same shape is reused by `judge-eval-variant`
4473
- * (#943) so tooling, dashboards, and analysis stay uniform.
4474
- */
4475
- var AssertionResult = _Object_({
4476
- id: String$1({ minLength: 1 }),
4477
- text: String$1({ minLength: 1 }),
4478
- passed: Boolean$1(),
4479
- evidence: String$1({ minLength: 1 })
4480
- }, {
4481
- $id: "AssertionResult",
4482
- additionalProperties: false
4483
- });
4484
- var RubricCriterion = _Object_({
4485
- id: String$1({ minLength: 1 }),
4486
- description: String$1({ minLength: 1 }),
4487
- weight: Number$1({
4488
- minimum: 0,
4489
- maximum: 1
4490
- }),
4491
- scoring: RubricScoringMode
4492
- }, {
4493
- $id: "RubricCriterion",
4494
- additionalProperties: false
4495
- });
4496
- /**
4497
- * A complete rubric. Same shape used in Phase 1 (inline) and Phase 2
4498
- * (stored row `body`); only the addressing mechanism differs.
4499
- */
4500
- var Rubric = _Object_({
4501
- rubricId: String$1({ minLength: 1 }),
4502
- version: String$1({ minLength: 1 }),
4503
- preamble: Optional(String$1()),
4504
- criteria: _Array_(RubricCriterion, { minItems: 1 }),
4505
- scope: Optional(String$1()),
4506
- contentHash: Optional(String$1())
4507
- }, {
4508
- $id: "Rubric",
4509
- additionalProperties: false
4510
- });
4511
- /**
4512
- * Verify rubric criteria weights sum to 1.0 within floating-point tolerance.
4513
- * The schema constrains each weight to [0,1] but can't express a cross-field
4514
- * sum constraint, so this is enforced programmatically by callers that
4515
- * accept rubrics (task input validators, server-side task creation).
4516
- *
4517
- * Returns null when valid; otherwise an error message suitable for surfacing
4518
- * to the caller. Tolerance is 1e-6 to accommodate JSON round-tripping of
4519
- * decimal fractions (e.g. 0.1 + 0.2 + 0.3 + 0.4 ≠ 1.0 exactly).
4520
- */
4521
- function validateRubricWeights(rubric) {
4522
- const sum = rubric.criteria.reduce((acc, c) => acc + c.weight, 0);
4523
- if (Math.abs(sum - 1) > 1e-6) return `Rubric weights must sum to 1.0 (got ${sum.toFixed(6)})`;
4524
- return null;
4525
- }
4526
- //#endregion
4527
4542
  //#region ../../libs/tasks/src/success-criteria.ts
4528
4543
  /**
4529
4544
  * SuccessCriteria — proposer-stated acceptance criteria, evaluated in two
@@ -9116,6 +9131,7 @@ var TaskAttemptStatus = Union([
9116
9131
  Literal("completed"),
9117
9132
  Literal("failed"),
9118
9133
  Literal("cancelled"),
9134
+ Literal("aborted"),
9119
9135
  Literal("timed_out")
9120
9136
  ], { $id: "TaskAttemptStatus" });
9121
9137
  var ExecutorTrustLevel = Union([
@@ -9268,7 +9284,7 @@ _Object_({
9268
9284
  acceptedAttemptN: Union([Number$1(), Null()]),
9269
9285
  claimCondition: Union([Unsafe(Ref$2("ClaimCondition")), Null()]),
9270
9286
  requiredExecutorTrustLevel: ExecutorTrustLevel,
9271
- allowedProfiles: _Array_(DaemonProfileRef, { maxItems: 16 }),
9287
+ allowedProfiles: _Array_(RuntimeProfileRef, { maxItems: 16 }),
9272
9288
  status: TaskStatus,
9273
9289
  queuedAt: IsoTimestamp,
9274
9290
  completedAt: Union([IsoTimestamp, Null()]),
@@ -9364,15 +9380,23 @@ _Object_({
9364
9380
  //#region src/lib/help.ts
9365
9381
  var COMMON_REQUIRED_FLAGS = `\
9366
9382
  -a, --agent <name> MoltNet agent identity. Reads credentials
9367
- from <repo-root>/.moltnet/<name>/moltnet.json.
9383
+ from <repo-root>/.moltnet/<name>/moltnet.json.`;
9384
+ var COMMON_MODEL_FLAGS = `\
9368
9385
  -p, --provider <id> LLM provider id (e.g. anthropic, openai-codex).
9369
9386
  -m, --model <id> LLM model id for the provider (e.g.
9370
- claude-sonnet-4-5, gpt-5.3-codex).`;
9387
+ claude-sonnet-4-5, gpt-5.3-codex). Required
9388
+ unless --profile is set.`;
9371
9389
  var COMMON_OPTIONAL_FLAGS = `\
9372
9390
  --sandbox <path> Path to sandbox.json. Default: search up from
9373
9391
  the daemon's CWD until found. The directory
9374
9392
  containing sandbox.json is also used as the
9375
- 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.
9376
9400
  --lease-ttl-sec <n> Sliding liveness window. Silence longer than
9377
9401
  this ends the attempt with lease_expired.
9378
9402
  Default: 300.
@@ -9390,7 +9414,8 @@ var COMMON_OPTIONAL_FLAGS = `\
9390
9414
  --warm-session-ttl-sec <n> Retain resumable daemon slots (Pi sessions +
9391
9415
  reusable worktrees) in local daemon state for
9392
9416
  this many seconds after use. 0 = disable reuse.
9393
- Default: 1800.
9417
+ Default: 1800, or min(profile session/workspace
9418
+ TTL) when --profile is set.
9394
9419
  --debug Verbose logging: also log successful list/claim
9395
9420
  outcomes (candidate counts, claim attempts).`;
9396
9421
  var REGISTERED_TASK_TYPES = Object.keys(BUILT_IN_TASK_TYPES).sort();
@@ -9413,9 +9438,10 @@ Run \`agent-daemon <command> --help\` for command-specific flags.
9413
9438
 
9414
9439
  Prerequisites (all subcommands):
9415
9440
  - <repo-root>/.moltnet/<agent>/moltnet.json — credentials (see --agent)
9416
- - sandbox.json — Gondolin snapshot config; resolved by searching up
9417
- from CWD, or pass --sandbox <path>. Its containing directory is the
9418
- 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.
9419
9445
 
9420
9446
  Registered task types: ${knownTaskTypesList()}`;
9421
9447
  var POLL_HELP = `\
@@ -9428,6 +9454,7 @@ Required:
9428
9454
  --team <uuid> Team whose queue to serve. The daemon must be
9429
9455
  a member of this team (canAccessTeam permit).
9430
9456
  ${COMMON_REQUIRED_FLAGS}
9457
+ ${COMMON_MODEL_FLAGS}
9431
9458
 
9432
9459
  Optional:
9433
9460
  --task-types <csv> Whitelist of task types to claim. Default:
@@ -9458,6 +9485,7 @@ Required:
9458
9485
  -t, --task-id <uuid> Task to claim and execute. Must already be
9459
9486
  in 'queued' status.
9460
9487
  ${COMMON_REQUIRED_FLAGS}
9488
+ ${COMMON_MODEL_FLAGS}
9461
9489
 
9462
9490
  Optional:
9463
9491
  ${COMMON_OPTIONAL_FLAGS}
@@ -9483,6 +9511,7 @@ sleeps and retries forever).
9483
9511
  Required:
9484
9512
  --team <uuid> Team whose queue to drain.
9485
9513
  ${COMMON_REQUIRED_FLAGS}
9514
+ ${COMMON_MODEL_FLAGS}
9486
9515
 
9487
9516
  Optional:
9488
9517
  --task-types <csv> Whitelist. Known types: ${knownTaskTypesList()}
@@ -24459,6 +24488,9 @@ var ApiTaskReporter = class {
24459
24488
  get cancelReason() {
24460
24489
  return this.observedCancelReason;
24461
24490
  }
24491
+ requestCancel(reason) {
24492
+ this.abortForCancel(reason);
24493
+ }
24462
24494
  async open(ctx) {
24463
24495
  if (this.heartbeatTimer) {
24464
24496
  clearInterval(this.heartbeatTimer);
@@ -24641,13 +24673,15 @@ var ApiTaskReporter = class {
24641
24673
  async sendHeartbeat() {
24642
24674
  const body = this.opts.leaseTtlSec ? { leaseTtlSec: this.opts.leaseTtlSec } : {};
24643
24675
  const response = await this.opts.tasks.heartbeat(this.taskId, this.attemptN, body);
24644
- if (response?.cancelled && !this.cancelController.signal.aborted) {
24645
- this.observedCancelReason = response.cancelReason ?? null;
24646
- this.cancelController.abort(/* @__PURE__ */ new Error(`Task cancelled by proposer${this.observedCancelReason ? `: ${this.observedCancelReason}` : ""}`));
24647
- if (this.heartbeatTimer) {
24648
- clearInterval(this.heartbeatTimer);
24649
- this.heartbeatTimer = null;
24650
- }
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;
24651
24685
  }
24652
24686
  }
24653
24687
  };
@@ -24660,6 +24694,7 @@ var AgentRuntime = class {
24660
24694
  currentTaskId: null
24661
24695
  };
24662
24696
  stopRequested = false;
24697
+ currentReporter = null;
24663
24698
  logger;
24664
24699
  constructor(opts) {
24665
24700
  this.opts = opts;
@@ -24688,6 +24723,7 @@ var AgentRuntime = class {
24688
24723
  });
24689
24724
  taskLogger.info({}, "agent-runtime.task_claimed");
24690
24725
  const reporter = this.opts.makeReporter(claimedTask);
24726
+ this.currentReporter = reporter;
24691
24727
  const taskCtx = Object.keys(claimedTask.traceHeaders).length ? propagation.extract(ROOT_CONTEXT, claimedTask.traceHeaders) : context.active();
24692
24728
  const taskStart = Date.now();
24693
24729
  let output;
@@ -24745,16 +24781,19 @@ var AgentRuntime = class {
24745
24781
  }
24746
24782
  this.status.tasksProcessed += 1;
24747
24783
  this.status.currentTaskId = null;
24784
+ this.currentReporter = null;
24748
24785
  }
24749
24786
  } finally {
24787
+ this.currentReporter = null;
24750
24788
  await this.opts.source.close();
24751
24789
  this.status.state = "stopped";
24752
24790
  }
24753
24791
  return outputs;
24754
24792
  }
24755
24793
  /** Request cooperative shutdown. Safe from signal handlers. */
24756
- stop() {
24794
+ stop(reason) {
24757
24795
  this.stopRequested = true;
24796
+ if (reason !== void 0) this.currentReporter?.requestCancel?.(reason);
24758
24797
  }
24759
24798
  };
24760
24799
  //#endregion
@@ -24766,8 +24805,11 @@ var ApiTaskSource = class {
24766
24805
  }
24767
24806
  async claim() {
24768
24807
  if (this.claimed) return null;
24769
- const { agent, taskId, leaseTtlSec } = this.opts;
24770
- 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
+ });
24771
24813
  this.claimed = true;
24772
24814
  return {
24773
24815
  task: result.task,
@@ -26412,6 +26454,124 @@ var updateRenderedPack = (options) => (options.client ?? client).patch({
26412
26454
  }
26413
26455
  });
26414
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
+ /**
26415
26575
  * List tasks for a team with optional filters.
26416
26576
  */
26417
26577
  var listTasks = (options) => (options.client ?? client).get({
@@ -26526,6 +26686,32 @@ var listTaskAttempts = (options) => (options.client ?? client).get({
26526
26686
  ...options
26527
26687
  });
26528
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
+ /**
26529
26715
  * Mark an attempt as completed with output.
26530
26716
  */
26531
26717
  var completeTask = (options) => (options.client ?? client).post({
@@ -28658,6 +28844,54 @@ function createRecoveryNamespace(context) {
28658
28844
  };
28659
28845
  }
28660
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
28661
28895
  //#region ../../libs/sdk/src/namespaces/signing-requests.ts
28662
28896
  function createSigningRequestsNamespace(context) {
28663
28897
  const { client, auth } = context;
@@ -28778,6 +29012,17 @@ function createTasksNamespace(context) {
28778
29012
  body
28779
29013
  }));
28780
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
+ },
28781
29026
  async cancel(id, body) {
28782
29027
  return unwrapResult(await cancelTask({
28783
29028
  client,
@@ -28961,6 +29206,7 @@ function createAgent(options) {
28961
29206
  legreffier: createLegreffierNamespace(context),
28962
29207
  problems: createProblemsNamespace(context),
28963
29208
  teams: createTeamsNamespace(context),
29209
+ runtimeProfiles: createRuntimeProfilesNamespace(context),
28964
29210
  tasks: createTasksNamespace(context),
28965
29211
  client,
28966
29212
  getToken: () => tokenManager.getToken()
@@ -31012,12 +31258,21 @@ if (!etc.sha512Sync) etc.sha512Sync = (...m) => {
31012
31258
  */
31013
31259
  async function isContinuationClaimableByThisDaemon(task, slotRegistry) {
31014
31260
  const cf = task.input?.continueFrom;
31015
- if (!cf) return true;
31261
+ if (!cf) return { claimable: true };
31016
31262
  const slot = await slotRegistry.findLatestProducerSlotByTaskAttempt(cf.taskId, cf.attemptN);
31017
- if (!slot) return false;
31263
+ if (!slot) return {
31264
+ claimable: false,
31265
+ reason: "missing_producer_slot",
31266
+ continueFrom: cf
31267
+ };
31018
31268
  const sessionDir = slot.session?.sessionDir;
31019
- if (!sessionDir || !existsSync(sessionDir)) return false;
31020
- return true;
31269
+ if (!sessionDir || !existsSync(sessionDir)) return {
31270
+ claimable: false,
31271
+ reason: "missing_session_dir",
31272
+ continueFrom: cf,
31273
+ sessionDir
31274
+ };
31275
+ return { claimable: true };
31021
31276
  }
31022
31277
  var DEFAULT_LIST_LIMIT = 10;
31023
31278
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
@@ -31105,7 +31360,19 @@ var PollingApiTaskSource = class {
31105
31360
  if (seen.has(item.id)) continue;
31106
31361
  if (this.opts.taskTypes && this.opts.taskTypes.length > 0 && !this.opts.taskTypes.includes(item.taskType)) continue;
31107
31362
  if (this.opts.diaryIds && this.opts.diaryIds.length > 0 && (item.diaryId === null || !this.opts.diaryIds.includes(item.diaryId))) continue;
31108
- if (this.opts.slotRegistry && !await isContinuationClaimableByThisDaemon(item, this.opts.slotRegistry)) continue;
31363
+ if (this.opts.slotRegistry) {
31364
+ const affinity = await isContinuationClaimableByThisDaemon(item, this.opts.slotRegistry);
31365
+ if (!affinity.claimable) {
31366
+ this.logger.debug({
31367
+ taskId: item.id,
31368
+ taskType: item.taskType,
31369
+ reason: affinity.reason,
31370
+ continueFrom: affinity.continueFrom,
31371
+ sessionDir: affinity.sessionDir
31372
+ }, "polling-api.continuation_skipped");
31373
+ continue;
31374
+ }
31375
+ }
31109
31376
  if (this.opts.profileId) {
31110
31377
  const allowed = item.allowedProfiles ?? [];
31111
31378
  if (allowed.length > 0 && !allowed.some((p) => p.profileId === this.opts.profileId)) continue;
@@ -32268,6 +32535,63 @@ function pruneOldSnapshots(maxCached, currentDir) {
32268
32535
  });
32269
32536
  }
32270
32537
  //#endregion
32538
+ //#region ../../libs/pi-extension/src/abort-utils.ts
32539
+ function throwIfAborted(signal, label) {
32540
+ if (!signal?.aborted) return;
32541
+ throw abortError(label, signal);
32542
+ }
32543
+ function abortError(label, signal) {
32544
+ const reason = signal.reason;
32545
+ const suffix = reason instanceof Error ? reason.message : reason === void 0 ? "aborted" : String(reason);
32546
+ const err = /* @__PURE__ */ new Error(`${label} aborted: ${suffix}`);
32547
+ err.name = "AbortError";
32548
+ return err;
32549
+ }
32550
+ function cleanupLateResource(resourcePromise, opts) {
32551
+ resourcePromise.then(async (resource) => {
32552
+ try {
32553
+ await opts.cleanup(resource);
32554
+ } catch (err) {
32555
+ opts.onCleanupError?.(err);
32556
+ }
32557
+ }, () => {});
32558
+ }
32559
+ async function abortableResource(opts) {
32560
+ const { signal } = opts;
32561
+ if (!signal) return opts.promise;
32562
+ throwIfAborted(signal, opts.label);
32563
+ const resourcePromise = Promise.resolve(opts.promise);
32564
+ const abortPromise = new Promise((_, reject) => {
32565
+ const abort = () => {
32566
+ cleanupLateResource(resourcePromise, opts);
32567
+ reject(abortError(opts.label, signal));
32568
+ };
32569
+ signal.addEventListener("abort", abort, { once: true });
32570
+ resourcePromise.then(() => signal.removeEventListener("abort", abort), () => signal.removeEventListener("abort", abort));
32571
+ });
32572
+ return Promise.race([resourcePromise, abortPromise]);
32573
+ }
32574
+ async function delay(ms, signal, label) {
32575
+ if (!signal) {
32576
+ await new Promise((resolve) => {
32577
+ setTimeout(resolve, ms);
32578
+ });
32579
+ return;
32580
+ }
32581
+ throwIfAborted(signal, label);
32582
+ await new Promise((resolve, reject) => {
32583
+ const listener = () => {
32584
+ clearTimeout(timeout);
32585
+ reject(abortError(label, signal));
32586
+ };
32587
+ const timeout = setTimeout(() => {
32588
+ signal.removeEventListener("abort", listener);
32589
+ resolve();
32590
+ }, ms);
32591
+ signal.addEventListener("abort", listener, { once: true });
32592
+ });
32593
+ }
32594
+ //#endregion
32271
32595
  //#region ../../libs/pi-extension/src/vm-manager.ts
32272
32596
  /**
32273
32597
  * Memory-backed VFS mount used by the daemon to inject task-context
@@ -32384,23 +32708,33 @@ var BASE_ALLOWED_HOSTS = [
32384
32708
  * surface immediately rather than fall through to cryptic agent
32385
32709
  * errors later.
32386
32710
  */
32387
- async function vmRun(vm, label, command) {
32711
+ async function vmRun(vm, label, command, signal) {
32388
32712
  const wrapped = `set -eu\nset -o pipefail\n${command}`;
32713
+ throwIfAborted(signal, `resume step "${label}"`);
32389
32714
  const r = await vm.exec([
32390
32715
  "sh",
32391
32716
  "-c",
32392
32717
  wrapped
32393
- ]);
32718
+ ], { signal });
32394
32719
  if (r.exitCode !== 0) {
32395
32720
  const tail = [r.stderr, r.stdout].filter(Boolean).join("\n").slice(-800);
32396
32721
  throw new Error(`resume step "${label}" failed (exit ${r.exitCode}):\n${tail}`);
32397
32722
  }
32398
32723
  }
32724
+ function nonErrorMessage(err) {
32725
+ if (typeof err === "string") return err;
32726
+ try {
32727
+ return JSON.stringify(err) ?? "unknown error";
32728
+ } catch {
32729
+ return "unknown error";
32730
+ }
32731
+ }
32399
32732
  /**
32400
32733
  * Resume a VM from a checkpoint, inject credentials, configure egress +
32401
32734
  * TLS. Returns the managed VM handle.
32402
32735
  */
32403
32736
  async function resumeVm(config) {
32737
+ throwIfAborted(config.signal, "VM resume");
32404
32738
  const mainRepo = findMainWorktree();
32405
32739
  const agentDir = path.join(mainRepo, ".moltnet", config.agentName);
32406
32740
  const guestWorkspace = path.resolve(config.mountPath);
@@ -32444,24 +32778,33 @@ async function resumeVm(config) {
32444
32778
  };
32445
32779
  const resources = config.sandboxConfig?.resources;
32446
32780
  const workspaceMode = config.workspaceMode ?? "shared_mount";
32447
- const vm = await VmCheckpoint.load(config.checkpointPath).resume({
32448
- httpHooks,
32449
- env: vmEnv,
32450
- ...resources?.memory && { memory: resources.memory },
32451
- ...resources?.cpus && { cpus: resources.cpus },
32452
- vfs: { mounts: {
32453
- [guestWorkspace]: workspaceProvider,
32454
- [GUEST_TASK_SKILLS_MOUNT]: new MemoryProvider()
32455
- } }
32781
+ const vm = await abortableResource({
32782
+ promise: VmCheckpoint.load(config.checkpointPath).resume({
32783
+ httpHooks,
32784
+ env: vmEnv,
32785
+ ...resources?.memory && { memory: resources.memory },
32786
+ ...resources?.cpus && { cpus: resources.cpus },
32787
+ vfs: { mounts: {
32788
+ [guestWorkspace]: workspaceProvider,
32789
+ [GUEST_TASK_SKILLS_MOUNT]: new MemoryProvider()
32790
+ } }
32791
+ }),
32792
+ signal: config.signal,
32793
+ label: "VM resume",
32794
+ cleanup: (resumedVm) => resumedVm.close(),
32795
+ onCleanupError: (err) => {
32796
+ const message = err instanceof Error ? err.message : String(err);
32797
+ process.stderr.write(`[vm] aborted resume late vm.close() failed: ${message}\n`);
32798
+ }
32456
32799
  });
32457
32800
  try {
32458
- await vm.exec(`sh -c '
32801
+ await vmRun(vm, "TLS certificates", `
32459
32802
  cp /etc/gondolin/mitm/ca.crt /usr/local/share/ca-certificates/gondolin-mitm.crt
32460
32803
  update-ca-certificates 2>/dev/null
32461
32804
  cat /etc/gondolin/mitm/ca.crt >> /etc/ssl/certs/ca-certificates.crt
32462
- '`);
32463
- await vmRun(vm, "DNS resolvers", `printf 'nameserver 8.8.8.8\\nnameserver 1.1.1.1\\n' > /etc/resolv.conf`);
32464
- await vmRun(vm, "git safe.directory", `git config --system --add safe.directory '*'`);
32805
+ `, config.signal);
32806
+ await vmRun(vm, "DNS resolvers", `printf 'nameserver 8.8.8.8\\nnameserver 1.1.1.1\\n' > /etc/resolv.conf`, config.signal);
32807
+ await vmRun(vm, "git safe.directory", `git config --system --add safe.directory '*'`, config.signal);
32465
32808
  for (const [i, entry] of (config.sandboxConfig?.resumeCommands ?? []).entries()) {
32466
32809
  if (!shouldRunResumeCommand(entry, { workspaceMode })) continue;
32467
32810
  const { run, retries, backoffMs } = typeof entry === "string" ? {
@@ -32476,34 +32819,67 @@ async function resumeVm(config) {
32476
32819
  const label = `resumeCommands[${i}]`;
32477
32820
  let lastErr;
32478
32821
  for (let attempt = 0; attempt <= retries; attempt++) try {
32479
- await vmRun(vm, label, run);
32822
+ await vmRun(vm, label, run, config.signal);
32480
32823
  lastErr = void 0;
32481
32824
  break;
32482
32825
  } catch (err) {
32483
32826
  lastErr = err;
32484
32827
  if (attempt === retries) break;
32485
- await new Promise((resolve) => {
32486
- setTimeout(resolve, (attempt + 1) * backoffMs);
32487
- });
32828
+ await delay((attempt + 1) * backoffMs, config.signal, label);
32488
32829
  }
32489
- if (lastErr) throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
32830
+ if (lastErr) throw lastErr instanceof Error ? lastErr : new Error(nonErrorMessage(lastErr));
32490
32831
  }
32491
32832
  const vmSshDir = `${vmAgentDir}/ssh`;
32492
- await vm.exec(`mkdir -p ${vmAgentDir}/ssh /home/agent/.pi/agent`);
32493
- if (creds.piAuthJson !== null) await vm.fs.writeFile("/home/agent/.pi/agent/auth.json", creds.piAuthJson, { mode: 384 });
32833
+ await vm.exec(`mkdir -p ${vmAgentDir}/ssh /home/agent/.pi/agent`, { signal: config.signal });
32834
+ if (creds.piAuthJson !== null) await vm.fs.writeFile("/home/agent/.pi/agent/auth.json", creds.piAuthJson, {
32835
+ mode: 384,
32836
+ signal: config.signal
32837
+ });
32494
32838
  const vmMoltnetJson = rewriteMoltnetJsonPaths(creds.moltnetJson, vmAgentDir, vmSshDir, creds.githubAppPemFilename);
32495
- await vm.fs.writeFile(`${vmAgentDir}/moltnet.json`, vmMoltnetJson, { mode: 384 });
32496
- await vm.fs.writeFile(`${vmAgentDir}/env`, creds.agentEnvRaw, { mode: 384 });
32839
+ await vm.fs.writeFile(`${vmAgentDir}/moltnet.json`, vmMoltnetJson, {
32840
+ mode: 384,
32841
+ signal: config.signal
32842
+ });
32843
+ await vm.fs.writeFile(`${vmAgentDir}/env`, creds.agentEnvRaw, {
32844
+ mode: 384,
32845
+ signal: config.signal
32846
+ });
32497
32847
  if (creds.gitconfig) {
32498
32848
  const vmSigningKey = `${vmSshDir}/id_ed25519`;
32499
32849
  const vmGitconfig = creds.gitconfig.replace(/signingKey\s*=\s*.+/g, `signingKey = ${vmSigningKey}`);
32500
- await vm.fs.writeFile(`${vmAgentDir}/gitconfig`, vmGitconfig, { mode: 420 });
32850
+ await vm.fs.writeFile(`${vmAgentDir}/gitconfig`, vmGitconfig, {
32851
+ mode: 420,
32852
+ signal: config.signal
32853
+ });
32501
32854
  }
32502
- if (creds.sshPrivateKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519`, creds.sshPrivateKey, { mode: 384 });
32503
- if (creds.sshPublicKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519.pub`, creds.sshPublicKey, { mode: 420 });
32504
- if (creds.allowedSigners) await vm.fs.writeFile(`${vmSshDir}/allowed_signers`, creds.allowedSigners, { mode: 420 });
32505
- if (creds.githubAppPem && creds.githubAppPemFilename) await vm.fs.writeFile(`${vmAgentDir}/${creds.githubAppPemFilename}`, creds.githubAppPem, { mode: 384 });
32506
- await vm.exec("chown -R agent:agent /home/agent/.pi /home/agent/.moltnet");
32855
+ if (creds.sshPrivateKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519`, creds.sshPrivateKey, {
32856
+ mode: 384,
32857
+ signal: config.signal
32858
+ });
32859
+ if (creds.sshPublicKey) await vm.fs.writeFile(`${vmSshDir}/id_ed25519.pub`, creds.sshPublicKey, {
32860
+ mode: 420,
32861
+ signal: config.signal
32862
+ });
32863
+ if (creds.allowedSigners) await vm.fs.writeFile(`${vmSshDir}/allowed_signers`, creds.allowedSigners, {
32864
+ mode: 420,
32865
+ signal: config.signal
32866
+ });
32867
+ if (creds.githubAppPem && creds.githubAppPemFilename) await vm.fs.writeFile(`${vmAgentDir}/${creds.githubAppPemFilename}`, creds.githubAppPem, {
32868
+ mode: 384,
32869
+ signal: config.signal
32870
+ });
32871
+ await vm.exec("chown -R agent:agent /home/agent/.pi /home/agent/.moltnet", { signal: config.signal });
32872
+ const gitCredHelperPath = `${vmSshDir}/git-credential-moltnet`;
32873
+ const credHelperScript = `#!/bin/sh
32874
+ echo "username=x-access-token"
32875
+ echo "password=$(moltnet github token --credentials ${vmSshDir}/moltnet.json)"
32876
+ `;
32877
+ await vm.fs.writeFile(gitCredHelperPath, credHelperScript, {
32878
+ mode: 493,
32879
+ signal: config.signal
32880
+ });
32881
+ await vmRun(vm, "git credential helper", `git config --global credential.helper ${gitCredHelperPath} && \
32882
+ git config --global url."https://github.com/".insteadOf "git@github.com:"`, config.signal);
32507
32883
  return {
32508
32884
  vm,
32509
32885
  credentials: creds,
@@ -33756,6 +34132,20 @@ async function executePiTask(claimedTask, reporter, opts) {
33756
34132
  retryable: false
33757
34133
  }
33758
34134
  });
34135
+ const makeCancelledOutput = (message) => ({
34136
+ taskId: task.id,
34137
+ attemptN,
34138
+ status: "cancelled",
34139
+ output: null,
34140
+ outputCid: null,
34141
+ usage: finalUsage,
34142
+ durationMs: Date.now() - startTime,
34143
+ error: {
34144
+ code: "task_cancelled",
34145
+ message,
34146
+ retryable: false
34147
+ }
34148
+ });
33759
34149
  let onTurnEvent;
33760
34150
  if (opts.makeOnTurnEvent) try {
33761
34151
  onTurnEvent = opts.makeOnTurnEvent(claimedTask);
@@ -33818,10 +34208,15 @@ async function executePiTask(claimedTask, reporter, opts) {
33818
34208
  mountPath,
33819
34209
  workspaceMode: workspace.mode,
33820
34210
  extraAllowedHosts: opts.extraAllowedHosts,
33821
- sandboxConfig
34211
+ sandboxConfig,
34212
+ signal: reporter.cancelSignal
33822
34213
  });
33823
34214
  } catch (err) {
33824
34215
  const message = err instanceof Error ? err.message : String(err);
34216
+ if (reporter.cancelSignal.aborted) {
34217
+ await emitError("vm_resume", message, { cancelled: true });
34218
+ return makeCancelledOutput(reporter.cancelReason ?? "Task cancelled during VM resume.");
34219
+ }
33825
34220
  await emitError("vm_resume", message);
33826
34221
  return makeFailedOutput("vm_resume_failed", message);
33827
34222
  }
@@ -34409,9 +34804,15 @@ function loadConfig() {
34409
34804
  return {
34410
34805
  agentDaemonStateDatabaseUrl: process.env["MOLTNET_AGENT_DAEMON_STATE_DATABASE_URL"] ?? "",
34411
34806
  otelEndpoint: process.env["MOLTNET_OTEL_ENDPOINT"] ?? "",
34412
- logLevel: process.env["LOG_LEVEL"] ?? ""
34807
+ logLevel: process.env["LOG_LEVEL"] ?? "",
34808
+ profilePrerequisiteEnv: process.env,
34809
+ profilePrerequisitePath: process.env.PATH ?? "",
34810
+ piCodingAgentDir: process.env["PI_CODING_AGENT_DIR"] ?? ""
34413
34811
  };
34414
34812
  }
34813
+ function activatePiCodingAgentDir(path) {
34814
+ process.env["PI_CODING_AGENT_DIR"] = path;
34815
+ }
34415
34816
  //#endregion
34416
34817
  //#region src/lib/agent-context.ts
34417
34818
  /**
@@ -34676,6 +35077,7 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
34676
35077
  return {
34677
35078
  ...basePlan,
34678
35079
  workspaceMode: "dedicated_worktree",
35080
+ workspaceId: resolution.producerSlot.workspace?.workspaceId ?? null,
34679
35081
  worktreeBranch: resolution.producerSlot.workspace?.worktreeBranch ?? null,
34680
35082
  sessionPersistence: {
34681
35083
  sessionDir: `${stateDirs.piSessionsDir}/continue-${claimedTask.task.id}-attempt-${claimedTask.attemptN}`,
@@ -34837,13 +35239,27 @@ function createRootLogger(options) {
34837
35239
  target: "pino-pretty",
34838
35240
  options: { colorize: true }
34839
35241
  }) : null;
35242
+ let transportClosed = prettyTransport === null;
35243
+ let shutdownStarted = false;
35244
+ prettyTransport?.on("close", () => {
35245
+ transportClosed = true;
35246
+ });
35247
+ prettyTransport?.on("error", (err) => {
35248
+ transportClosed = true;
35249
+ process.stderr.write(`[pino] transport error: ` + (err instanceof Error ? err.message : String(err)) + "\n");
35250
+ });
34840
35251
  const logger = pino(options, prettyTransport ?? void 0);
34841
35252
  const shutdown = async () => {
34842
- if (!prettyTransport) return;
34843
- logger.flush();
35253
+ if (!prettyTransport || shutdownStarted || transportClosed) return;
35254
+ shutdownStarted = true;
35255
+ try {
35256
+ logger.flush();
35257
+ } catch (err) {
35258
+ process.stderr.write(`[pino] logger flush failed: ` + (err instanceof Error ? err.message : String(err)) + "\n");
35259
+ }
34844
35260
  try {
34845
35261
  prettyTransport.end();
34846
- await once(prettyTransport, "close");
35262
+ if (!transportClosed) await once(prettyTransport, "close");
34847
35263
  } catch (err) {
34848
35264
  process.stderr.write(`[pino] transport teardown failed: ` + (err instanceof Error ? err.message : String(err)) + "\n");
34849
35265
  }
@@ -34871,22 +35287,29 @@ var MissingRequiredOptionError = class extends Error {
34871
35287
  this.name = "MissingRequiredOptionError";
34872
35288
  }
34873
35289
  };
34874
- function parseCommonOptions(args) {
35290
+ function parseCommonOptions(args, options = {}) {
35291
+ const requireProviderModel = options.requireProviderModel ?? true;
35292
+ const runtimeDefaults = {
35293
+ leaseTtlSec: options.runtimeDefaults?.leaseTtlSec ?? DEFAULTS.leaseTtlSec,
35294
+ heartbeatIntervalMs: options.runtimeDefaults?.heartbeatIntervalMs ?? DEFAULTS.heartbeatIntervalMs,
35295
+ maxBatchSize: options.runtimeDefaults?.maxBatchSize ?? DEFAULTS.maxBatchSize,
35296
+ warmSessionTtlSec: options.runtimeDefaults?.warmSessionTtlSec ?? DEFAULTS.warmSessionTtlSec
35297
+ };
34875
35298
  if (!args.agent) throw new MissingRequiredOptionError("agent");
34876
- if (!args.provider) throw new MissingRequiredOptionError("provider");
34877
- if (!args.model) throw new MissingRequiredOptionError("model");
35299
+ if (requireProviderModel && !args.provider) throw new MissingRequiredOptionError("provider");
35300
+ if (requireProviderModel && !args.model) throw new MissingRequiredOptionError("model");
34878
35301
  if (!/^[a-zA-Z0-9_-]+$/.test(args.agent)) throw new Error(`Invalid --agent "${args.agent}": must match /^[a-zA-Z0-9_-]+$/`);
34879
35302
  return {
34880
35303
  agent: args.agent,
34881
- provider: args.provider,
34882
- model: args.model,
34883
- leaseTtlSec: parsePositiveInt(args["lease-ttl-sec"], "lease-ttl-sec", DEFAULTS.leaseTtlSec),
34884
- heartbeatIntervalMs: parseNonNegativeInt(args["heartbeat-interval-ms"], "heartbeat-interval-ms", DEFAULTS.heartbeatIntervalMs),
34885
- maxBatchSize: parsePositiveInt(args["max-batch-size"], "max-batch-size", DEFAULTS.maxBatchSize),
35304
+ ...args.provider ? { provider: args.provider } : {},
35305
+ ...args.model ? { model: args.model } : {},
35306
+ leaseTtlSec: parsePositiveInt(args["lease-ttl-sec"], "lease-ttl-sec", runtimeDefaults.leaseTtlSec),
35307
+ heartbeatIntervalMs: parseNonNegativeInt(args["heartbeat-interval-ms"], "heartbeat-interval-ms", runtimeDefaults.heartbeatIntervalMs),
35308
+ maxBatchSize: parsePositiveInt(args["max-batch-size"], "max-batch-size", runtimeDefaults.maxBatchSize),
34886
35309
  flushIntervalMs: parseNonNegativeInt(args["flush-interval-ms"], "flush-interval-ms", DEFAULTS.flushIntervalMs),
34887
35310
  maxTurns: parseNonNegativeInt(args["max-turns"], "max-turns", DEFAULTS.maxTurns),
34888
35311
  maxBashTimeouts: parseNonNegativeInt(args["max-bash-timeouts"], "max-bash-timeouts", DEFAULTS.maxBashTimeouts),
34889
- warmSessionTtlSec: parseNonNegativeInt(args["warm-session-ttl-sec"], "warm-session-ttl-sec", DEFAULTS.warmSessionTtlSec),
35312
+ warmSessionTtlSec: parseNonNegativeInt(args["warm-session-ttl-sec"], "warm-session-ttl-sec", runtimeDefaults.warmSessionTtlSec),
34890
35313
  debug: args.debug === true
34891
35314
  };
34892
35315
  }
@@ -34977,6 +35400,88 @@ async function initWorkerOtel(options) {
34977
35400
  };
34978
35401
  }
34979
35402
  //#endregion
35403
+ //#region src/lib/pi-agent-dir.ts
35404
+ function ensurePiAgentDir(repoRoot, explicitPath) {
35405
+ if (explicitPath) {
35406
+ mkdirSync(explicitPath, { recursive: true });
35407
+ return {
35408
+ path: explicitPath,
35409
+ source: "env"
35410
+ };
35411
+ }
35412
+ const path = join(repoRoot, ".pi");
35413
+ mkdirSync(path, { recursive: true });
35414
+ return {
35415
+ path,
35416
+ source: "repo"
35417
+ };
35418
+ }
35419
+ //#endregion
35420
+ //#region src/lib/runtime-profile.ts
35421
+ var RuntimeProfilePrerequisiteError = class extends Error {
35422
+ constructor(profileName, missingEnv, missingTools) {
35423
+ const parts = [missingEnv.length > 0 ? `missing env: ${missingEnv.join(", ")}` : null, missingTools.length > 0 ? `missing tools: ${missingTools.join(", ")}` : null].filter(Boolean);
35424
+ super(`Runtime profile "${profileName}" prerequisites are not satisfied: ${parts.join("; ")}`);
35425
+ this.profileName = profileName;
35426
+ this.missingEnv = missingEnv;
35427
+ this.missingTools = missingTools;
35428
+ this.name = "RuntimeProfilePrerequisiteError";
35429
+ }
35430
+ };
35431
+ 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;
35432
+ async function resolveRuntimeProfile(options) {
35433
+ const profile = UUID_RE.test(options.profile) ? await options.agent.runtimeProfiles.get(options.profile) : await resolveProfileByName(options);
35434
+ if (options.teamId && profile.teamId !== options.teamId) throw new Error(`Runtime profile "${options.profile}" belongs to team ${profile.teamId}, not ${options.teamId}.`);
35435
+ return {
35436
+ id: profile.id,
35437
+ name: profile.name,
35438
+ teamId: profile.teamId,
35439
+ provider: profile.provider,
35440
+ model: profile.model,
35441
+ leaseTtlSec: profile.leaseTtlSec,
35442
+ heartbeatIntervalMs: profile.heartbeatIntervalMs,
35443
+ maxBatchSize: profile.maxBatchSize,
35444
+ sessionTtlSec: profile.sessionTtlSec,
35445
+ workspaceTtlSec: profile.workspaceTtlSec,
35446
+ requiredEnv: profile.requiredEnv,
35447
+ requiredTools: profile.requiredTools,
35448
+ sandboxConfig: profile.sandbox,
35449
+ mountPath: resolve(options.cwd),
35450
+ source: `runtime-profile:${profile.id}`
35451
+ };
35452
+ }
35453
+ function validateRuntimeProfilePrerequisites(profile, env, pathValue) {
35454
+ const missingEnv = profile.requiredEnv.filter((name) => !env[name]);
35455
+ const missingTools = profile.requiredTools.filter((tool) => !isExecutableOnPath(tool, pathValue));
35456
+ if (missingEnv.length > 0 || missingTools.length > 0) throw new RuntimeProfilePrerequisiteError(profile.name, missingEnv, missingTools);
35457
+ }
35458
+ function resolveProfileWarmSessionTtlSec(profile) {
35459
+ return Math.min(profile.sessionTtlSec, profile.workspaceTtlSec);
35460
+ }
35461
+ async function resolveProfileByName(options) {
35462
+ if (!options.teamId) throw new Error(`Runtime profile name "${options.profile}" requires --team. Use a profile UUID when running without a team-scoped list.`);
35463
+ const matches = (await options.agent.runtimeProfiles.list({ teamId: options.teamId })).items.filter((item) => item.name === options.profile);
35464
+ if (matches.length === 0) throw new Error(`Runtime profile "${options.profile}" was not found in team ${options.teamId}.`);
35465
+ if (matches.length > 1) throw new Error(`Runtime profile name "${options.profile}" is ambiguous in team ${options.teamId}. Use the profile UUID instead.`);
35466
+ return matches[0];
35467
+ }
35468
+ function isExecutableOnPath(tool, pathValue) {
35469
+ if (tool.includes("/")) return isExecutable(isAbsolute(tool) ? tool : resolve(process.cwd(), tool));
35470
+ for (const dir of (pathValue ?? "").split(delimiter)) {
35471
+ if (!dir) continue;
35472
+ if (isExecutable(resolve(dir, tool))) return true;
35473
+ }
35474
+ return false;
35475
+ }
35476
+ function isExecutable(path) {
35477
+ try {
35478
+ accessSync(path, constants.X_OK);
35479
+ return true;
35480
+ } catch {
35481
+ return false;
35482
+ }
35483
+ }
35484
+ //#endregion
34980
35485
  //#region src/lib/sandbox.ts
34981
35486
  function resolveSandbox(startDir, explicitPath) {
34982
35487
  const path = explicitPath ? isAbsolute(explicitPath) ? explicitPath : resolve(startDir, explicitPath) : findUp(startDir, "sandbox.json");
@@ -35005,6 +35510,38 @@ function findUp(startDir, filename) {
35005
35510
  }
35006
35511
  }
35007
35512
  //#endregion
35513
+ //#region src/lib/shutdown-signal.ts
35514
+ function installShutdownSignalHandlers(opts) {
35515
+ const proc = opts.proc ?? process;
35516
+ let drainingSignal = null;
35517
+ const onSignal = (signal) => {
35518
+ if (drainingSignal) {
35519
+ proc.stderr.write(`[agent-daemon] ${signal} received while already draining from ${drainingSignal}; waiting for cleanup.\n`);
35520
+ proc.exitCode = signalExitCode(signal);
35521
+ return;
35522
+ }
35523
+ drainingSignal = signal;
35524
+ proc.exitCode = signalExitCode(signal);
35525
+ try {
35526
+ opts.logDrain(signal);
35527
+ } catch (err) {
35528
+ proc.stderr.write(`[agent-daemon] failed to log ${signal}: ` + (err instanceof Error ? err.message : String(err)) + "\n");
35529
+ }
35530
+ opts.drain(signal);
35531
+ };
35532
+ const handleSigint = () => onSignal("SIGINT");
35533
+ const handleSigterm = () => onSignal("SIGTERM");
35534
+ proc.on("SIGINT", handleSigint);
35535
+ proc.on("SIGTERM", handleSigterm);
35536
+ return { dispose: () => {
35537
+ proc.off("SIGINT", handleSigint);
35538
+ proc.off("SIGTERM", handleSigterm);
35539
+ } };
35540
+ }
35541
+ function signalExitCode(signal) {
35542
+ return signal === "SIGINT" ? 130 : 143;
35543
+ }
35544
+ //#endregion
35008
35545
  //#region src/lib/state-dir.ts
35009
35546
  function ensureDaemonStateDirs(mountPath) {
35010
35547
  const rootDir = join(mountPath, ".moltnet", "d");
@@ -35054,7 +35591,8 @@ async function runPolling(opts) {
35054
35591
  "poll-interval-ms": { type: "string" },
35055
35592
  "max-poll-interval-ms": { type: "string" },
35056
35593
  "list-limit": { type: "string" },
35057
- sandbox: { type: "string" }
35594
+ sandbox: { type: "string" },
35595
+ profile: { type: "string" }
35058
35596
  }
35059
35597
  });
35060
35598
  if (!values.team) {
@@ -35074,7 +35612,7 @@ async function runPolling(opts) {
35074
35612
  const diaryIds = parseCsv(values["diary-ids"]);
35075
35613
  let common;
35076
35614
  try {
35077
- common = parseCommonOptions(values);
35615
+ common = parseCommonOptions(values, { requireProviderModel: !values.profile });
35078
35616
  } catch (err) {
35079
35617
  if (err instanceof MissingRequiredOptionError) {
35080
35618
  console.error(`${err.message}\n`);
@@ -35086,15 +35624,47 @@ async function runPolling(opts) {
35086
35624
  const pollIntervalMs = optionalPositiveInt(values["poll-interval-ms"], "poll-interval-ms", 2e3);
35087
35625
  const maxPollIntervalMs = optionalPositiveInt(values["max-poll-interval-ms"], "max-poll-interval-ms", 3e4);
35088
35626
  const listLimit = optionalPositiveInt(values["list-limit"], "list-limit", 10);
35627
+ if (values.profile && values.sandbox) {
35628
+ console.error(`[${opts.modeLabel}] Cannot use --sandbox with --profile. Remote runtime profiles define sandbox policy.`);
35629
+ return 1;
35630
+ }
35089
35631
  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).`);
35090
- const sandbox = resolveSandbox(process.cwd(), values.sandbox);
35091
35632
  const cfg = loadConfig();
35633
+ const ctx = await resolveAgentContext(common.agent);
35634
+ const profile = values.profile ? await resolveRuntimeProfile({
35635
+ agent: ctx.agent,
35636
+ profile: values.profile,
35637
+ teamId,
35638
+ cwd: process.cwd()
35639
+ }) : null;
35640
+ if (profile) {
35641
+ validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
35642
+ common = parseCommonOptions(values, {
35643
+ requireProviderModel: false,
35644
+ runtimeDefaults: {
35645
+ leaseTtlSec: profile.leaseTtlSec,
35646
+ heartbeatIntervalMs: profile.heartbeatIntervalMs,
35647
+ maxBatchSize: profile.maxBatchSize,
35648
+ warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
35649
+ }
35650
+ });
35651
+ }
35652
+ const provider = profile?.provider ?? common.provider;
35653
+ const model = profile?.model ?? common.model;
35654
+ if (!provider || !model) throw new Error("provider/model missing after runtime profile resolution");
35655
+ const sandbox = profile ? {
35656
+ config: profile.sandboxConfig,
35657
+ rootDir: profile.mountPath,
35658
+ path: profile.source
35659
+ } : resolveSandbox(process.cwd(), values.sandbox);
35660
+ const piAgentDir = ensurePiAgentDir(sandbox.rootDir, cfg.piCodingAgentDir);
35661
+ activatePiCodingAgentDir(piAgentDir.path);
35092
35662
  const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
35093
35663
  const slotRegistry = new DaemonSlotRegistry(resolveDaemonStateStorageConfig(stateDirs.registryDbPath, cfg.agentDaemonStateDatabaseUrl));
35094
35664
  const slotIdentity = {
35095
35665
  agentName: common.agent,
35096
- provider: common.provider,
35097
- model: common.model
35666
+ provider,
35667
+ model
35098
35668
  };
35099
35669
  const mainRepo = findMainWorktree();
35100
35670
  const executionPlans = createExecutionPlanCache({
@@ -35103,7 +35673,6 @@ async function runPolling(opts) {
35103
35673
  warmSessionTtlSec: common.warmSessionTtlSec,
35104
35674
  slotRegistry
35105
35675
  });
35106
- const ctx = await resolveAgentContext(common.agent);
35107
35676
  const otelShutdown = await initWorkerOtel({
35108
35677
  serviceName: opts.serviceName,
35109
35678
  agentDir: ctx.agentDir,
@@ -35111,8 +35680,9 @@ async function runPolling(opts) {
35111
35680
  resourceAttributes: {
35112
35681
  "moltnet.team.id": teamId,
35113
35682
  "moltnet.agent.name": common.agent,
35114
- "moltnet.llm.provider": common.provider,
35115
- "moltnet.llm.model": common.model
35683
+ "moltnet.llm.provider": provider,
35684
+ "moltnet.llm.model": model,
35685
+ ...profile ? { "moltnet.daemon_profile.id": profile.id } : {}
35116
35686
  }
35117
35687
  });
35118
35688
  const { logger, shutdown: shutdownLogger } = createRootLogger({
@@ -35123,46 +35693,84 @@ async function runPolling(opts) {
35123
35693
  mode: opts.modeLabel,
35124
35694
  agent: common.agent,
35125
35695
  teamId,
35126
- provider: common.provider,
35127
- model: common.model
35696
+ provider,
35697
+ model,
35698
+ ...profile ? {
35699
+ daemonProfileId: profile.id,
35700
+ daemonProfileName: profile.name
35701
+ } : {}
35128
35702
  });
35129
35703
  const abort = new AbortController();
35130
35704
  let runtime = null;
35131
- const onSignal = (sig) => {
35132
- rootLogger.warn({ signal: sig }, "agent-daemon.draining");
35133
- abort.abort();
35134
- runtime?.stop();
35135
- };
35136
- process.on("SIGINT", () => onSignal("SIGINT"));
35137
- process.on("SIGTERM", () => onSignal("SIGTERM"));
35705
+ let active = null;
35706
+ const signalHandlers = installShutdownSignalHandlers({
35707
+ logDrain: (signal) => {
35708
+ rootLogger.warn({ signal }, "agent-daemon.draining");
35709
+ },
35710
+ drain: (signal) => {
35711
+ abort.abort();
35712
+ runtime?.stop(`agent-daemon received ${signal}`);
35713
+ if (active === null) return;
35714
+ const { taskId, attemptN } = active;
35715
+ ctx.agent.tasks.abortAttempt(taskId, attemptN, { reason: `runner_${signal.toLowerCase()}` }).catch((err) => {
35716
+ try {
35717
+ rootLogger.warn({
35718
+ err: err instanceof Error ? err.message : String(err),
35719
+ taskId,
35720
+ attemptN
35721
+ }, "agent-daemon.abort_on_signal_failed");
35722
+ } catch {}
35723
+ });
35724
+ }
35725
+ });
35138
35726
  rootLogger.info({
35139
35727
  sandbox: sandbox.path,
35140
35728
  taskTypes: taskTypes.length > 0 ? taskTypes : ["*"],
35141
35729
  diaryIds: diaryIds.length > 0 ? diaryIds : ["*"],
35142
35730
  leaseTtlSec: common.leaseTtlSec,
35143
35731
  heartbeatIntervalMs: common.heartbeatIntervalMs,
35732
+ warmSessionTtlSec: common.warmSessionTtlSec,
35144
35733
  pollIntervalMs,
35145
- maxPollIntervalMs
35734
+ maxPollIntervalMs,
35735
+ ...profile ? {
35736
+ profileId: profile.id,
35737
+ profileSessionTtlSec: profile.sessionTtlSec,
35738
+ profileWorkspaceTtlSec: profile.workspaceTtlSec
35739
+ } : {},
35740
+ piAgentDir: piAgentDir.path,
35741
+ piAgentDirSource: piAgentDir.source
35146
35742
  }, "agent-daemon.starting");
35147
35743
  const outputs = [];
35148
35744
  try {
35149
- const executeTask = createPiTaskExecutor({
35745
+ const rawExecuteTask = createPiTaskExecutor({
35150
35746
  agentName: common.agent,
35151
35747
  mountPath: sandbox.rootDir,
35152
- provider: common.provider,
35153
- model: common.model,
35748
+ provider,
35749
+ model,
35154
35750
  sandboxConfig: sandbox.config,
35155
35751
  makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
35156
35752
  makeOnTurnEvent: makeTurnEventHandlerFactory(rootLogger),
35157
35753
  maxTurns: common.maxTurns,
35158
35754
  maxBashTimeouts: common.maxBashTimeouts
35159
35755
  });
35756
+ const executeTask = async (claimedTask, reporter) => {
35757
+ active = {
35758
+ taskId: claimedTask.task.id,
35759
+ attemptN: claimedTask.attemptN
35760
+ };
35761
+ try {
35762
+ return await rawExecuteTask(claimedTask, reporter);
35763
+ } finally {
35764
+ active = null;
35765
+ }
35766
+ };
35160
35767
  runtime = new AgentRuntime({
35161
35768
  logger: rootLogger,
35162
35769
  source: new PollingApiTaskSource({
35163
35770
  agent: ctx.agent,
35164
35771
  teamId,
35165
35772
  taskTypes: taskTypes.length > 0 ? taskTypes : void 0,
35773
+ ...profile ? { profileId: profile.id } : {},
35166
35774
  diaryIds: diaryIds.length > 0 ? diaryIds : void 0,
35167
35775
  leaseTtlSec: common.leaseTtlSec,
35168
35776
  listLimit,
@@ -35309,6 +35917,7 @@ async function runPolling(opts) {
35309
35917
  rootLogger.info({ processed: drained.length }, "agent-daemon.drained");
35310
35918
  return drained.some((o) => o.status !== "completed") ? 1 : 0;
35311
35919
  } finally {
35920
+ signalHandlers.dispose();
35312
35921
  await slotRegistry.close();
35313
35922
  await otelShutdown();
35314
35923
  await shutdownLogger();
@@ -35353,7 +35962,8 @@ async function runOnce(argv) {
35353
35962
  type: "string",
35354
35963
  short: "t"
35355
35964
  },
35356
- sandbox: { type: "string" }
35965
+ sandbox: { type: "string" },
35966
+ profile: { type: "string" }
35357
35967
  }
35358
35968
  });
35359
35969
  if (!values["task-id"]) {
@@ -35364,7 +35974,7 @@ async function runOnce(argv) {
35364
35974
  const taskId = values["task-id"];
35365
35975
  let opts;
35366
35976
  try {
35367
- opts = parseCommonOptions(values);
35977
+ opts = parseCommonOptions(values, { requireProviderModel: !values.profile });
35368
35978
  } catch (err) {
35369
35979
  if (err instanceof MissingRequiredOptionError) {
35370
35980
  console.error(`${err.message}\n`);
@@ -35373,14 +35983,45 @@ async function runOnce(argv) {
35373
35983
  }
35374
35984
  throw err;
35375
35985
  }
35376
- const sandbox = resolveSandbox(process.cwd(), values.sandbox);
35986
+ if (values.profile && values.sandbox) {
35987
+ console.error("Cannot use --sandbox with --profile. Remote runtime profiles define sandbox policy.");
35988
+ return 1;
35989
+ }
35377
35990
  const cfg = loadConfig();
35991
+ const ctx = await resolveAgentContext(opts.agent);
35992
+ const profile = values.profile ? await resolveRuntimeProfile({
35993
+ agent: ctx.agent,
35994
+ profile: values.profile,
35995
+ cwd: process.cwd()
35996
+ }) : null;
35997
+ if (profile) {
35998
+ validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
35999
+ opts = parseCommonOptions(values, {
36000
+ requireProviderModel: false,
36001
+ runtimeDefaults: {
36002
+ leaseTtlSec: profile.leaseTtlSec,
36003
+ heartbeatIntervalMs: profile.heartbeatIntervalMs,
36004
+ maxBatchSize: profile.maxBatchSize,
36005
+ warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
36006
+ }
36007
+ });
36008
+ }
36009
+ const provider = profile?.provider ?? opts.provider;
36010
+ const model = profile?.model ?? opts.model;
36011
+ if (!provider || !model) throw new Error("provider/model missing after runtime profile resolution");
36012
+ const sandbox = profile ? {
36013
+ config: profile.sandboxConfig,
36014
+ rootDir: profile.mountPath,
36015
+ path: profile.source
36016
+ } : resolveSandbox(process.cwd(), values.sandbox);
36017
+ const piAgentDir = ensurePiAgentDir(sandbox.rootDir, cfg.piCodingAgentDir);
36018
+ activatePiCodingAgentDir(piAgentDir.path);
35378
36019
  const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
35379
36020
  const slotRegistry = new DaemonSlotRegistry(resolveDaemonStateStorageConfig(stateDirs.registryDbPath, cfg.agentDaemonStateDatabaseUrl));
35380
36021
  const slotIdentity = {
35381
36022
  agentName: opts.agent,
35382
- provider: opts.provider,
35383
- model: opts.model
36023
+ provider,
36024
+ model
35384
36025
  };
35385
36026
  const mainRepo = findMainWorktree();
35386
36027
  const executionPlans = createExecutionPlanCache({
@@ -35389,7 +36030,6 @@ async function runOnce(argv) {
35389
36030
  warmSessionTtlSec: opts.warmSessionTtlSec,
35390
36031
  slotRegistry
35391
36032
  });
35392
- const ctx = await resolveAgentContext(opts.agent);
35393
36033
  const otelShutdown = await initWorkerOtel({
35394
36034
  serviceName: "moltnet.agent-daemon.once",
35395
36035
  agentDir: ctx.agentDir,
@@ -35397,8 +36037,9 @@ async function runOnce(argv) {
35397
36037
  resourceAttributes: {
35398
36038
  "moltnet.task.id": taskId,
35399
36039
  "moltnet.agent.name": opts.agent,
35400
- "moltnet.llm.provider": opts.provider,
35401
- "moltnet.llm.model": opts.model
36040
+ "moltnet.llm.provider": provider,
36041
+ "moltnet.llm.model": model,
36042
+ ...profile ? { "moltnet.daemon_profile.id": profile.id } : {}
35402
36043
  }
35403
36044
  });
35404
36045
  const { logger, shutdown: shutdownLogger } = createRootLogger({
@@ -35408,39 +36049,59 @@ async function runOnce(argv) {
35408
36049
  const rootLogger = logger.child({
35409
36050
  mode: "once",
35410
36051
  agent: opts.agent,
35411
- provider: opts.provider,
35412
- model: opts.model
36052
+ provider,
36053
+ model,
36054
+ ...profile ? {
36055
+ daemonProfileId: profile.id,
36056
+ daemonProfileName: profile.name
36057
+ } : {}
35413
36058
  });
35414
36059
  rootLogger.info({
35415
36060
  sandbox: sandbox.path,
35416
- taskId
36061
+ taskId,
36062
+ leaseTtlSec: opts.leaseTtlSec,
36063
+ heartbeatIntervalMs: opts.heartbeatIntervalMs,
36064
+ warmSessionTtlSec: opts.warmSessionTtlSec,
36065
+ ...profile ? {
36066
+ profileId: profile.id,
36067
+ profileSessionTtlSec: profile.sessionTtlSec,
36068
+ profileWorkspaceTtlSec: profile.workspaceTtlSec
36069
+ } : {},
36070
+ piAgentDir: piAgentDir.path,
36071
+ piAgentDirSource: piAgentDir.source
35417
36072
  }, "agent-daemon.starting");
35418
36073
  let runtime = null;
35419
- const onSignal = (sig) => {
35420
- rootLogger.warn({
35421
- signal: sig,
35422
- taskId
35423
- }, "agent-daemon.draining");
35424
- runtime?.stop();
35425
- ctx.agent.tasks.cancel(taskId, { reason: `runner_${sig.toLowerCase()}` }).catch((err) => {
36074
+ let activeAttemptN = null;
36075
+ const signalHandlers = installShutdownSignalHandlers({
36076
+ logDrain: (signal) => {
35426
36077
  rootLogger.warn({
35427
- err: err instanceof Error ? err.message : String(err),
36078
+ signal,
35428
36079
  taskId
35429
- }, "agent-daemon.cancel_on_signal_failed");
35430
- });
35431
- };
35432
- process.on("SIGINT", () => {
35433
- onSignal("SIGINT");
35434
- });
35435
- process.on("SIGTERM", () => {
35436
- onSignal("SIGTERM");
36080
+ }, "agent-daemon.draining");
36081
+ },
36082
+ drain: (signal) => {
36083
+ runtime?.stop(`agent-daemon received ${signal}`);
36084
+ if (activeAttemptN === null) return;
36085
+ const attemptN = activeAttemptN;
36086
+ ctx.agent.tasks.abortAttempt(taskId, attemptN, { reason: `runner_${signal.toLowerCase()}` }).catch((err) => {
36087
+ try {
36088
+ rootLogger.warn({
36089
+ err: err instanceof Error ? err.message : String(err),
36090
+ taskId,
36091
+ attemptN
36092
+ }, "agent-daemon.abort_on_signal_failed");
36093
+ } catch (logErr) {
36094
+ process.stderr.write(`[agent-daemon] failed to log abort error: ` + (logErr instanceof Error ? logErr.message : String(logErr)) + "\n");
36095
+ }
36096
+ });
36097
+ }
35437
36098
  });
35438
36099
  try {
35439
36100
  const rawExecuteTask = createPiTaskExecutor({
35440
36101
  agentName: opts.agent,
35441
36102
  mountPath: sandbox.rootDir,
35442
- provider: opts.provider,
35443
- model: opts.model,
36103
+ provider,
36104
+ model,
35444
36105
  sandboxConfig: sandbox.config,
35445
36106
  makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
35446
36107
  onTurnEvent: makeTurnEventHandler(rootLogger, { taskId }),
@@ -35501,9 +36162,11 @@ async function runOnce(argv) {
35501
36162
  lastAttemptN: claimedTask.attemptN,
35502
36163
  ttlSec: opts.warmSessionTtlSec
35503
36164
  });
36165
+ activeAttemptN = claimedTask.attemptN;
35504
36166
  try {
35505
36167
  return await rawExecuteTask(claimedTask, reporter);
35506
36168
  } finally {
36169
+ activeAttemptN = null;
35507
36170
  executionPlans.delete(claimedTask);
35508
36171
  if (executionPlan.slotKey) await slotRegistry.finishSlot(slotIdentity, executionPlan.slotKey, opts.warmSessionTtlSec, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null);
35509
36172
  }
@@ -35517,7 +36180,8 @@ async function runOnce(argv) {
35517
36180
  source: new ApiTaskSource({
35518
36181
  agent: ctx.agent,
35519
36182
  taskId,
35520
- leaseTtlSec: opts.leaseTtlSec
36183
+ leaseTtlSec: opts.leaseTtlSec,
36184
+ ...profile ? { profileId: profile.id } : {}
35521
36185
  }),
35522
36186
  makeReporter: () => new ApiTaskReporter({
35523
36187
  tasks: ctx.agent.tasks,
@@ -35546,6 +36210,7 @@ async function runOnce(argv) {
35546
36210
  console.log(JSON.stringify(output, null, 2));
35547
36211
  return output.status === "completed" ? 0 : 1;
35548
36212
  } finally {
36213
+ signalHandlers.dispose();
35549
36214
  await slotRegistry.close();
35550
36215
  await otelShutdown();
35551
36216
  await shutdownLogger();