@themoltnet/agent-daemon 0.19.1 → 0.21.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 +14 -20
  2. package/dist/main.js +429 -263
  3. package/package.json +8 -10
package/README.md CHANGED
@@ -80,8 +80,8 @@ To force a non-repo Pi directory, set
80
80
 
81
81
  ### Host command auto-approval
82
82
 
83
- The daemon reads `sandbox.json` through `--sandbox` or by searching up from the
84
- current directory. Configure host-side auto-approval there, not in task data:
83
+ The daemon reads host-side auto-approval from the selected remote runtime
84
+ profile's sandbox policy. Configure it in the profile, not in task data:
85
85
 
86
86
  ```json
87
87
  {
@@ -143,12 +143,11 @@ registered task type; unknown task-type names remain invalid.
143
143
  gitignored. For API-key auth, keep `.pi/auth.json` absent and export the
144
144
  provider key referenced by `.pi/models.json`, for example `OLLAMA_API_KEY`.
145
145
  - `ssh-keygen` on `PATH`.
146
- - A `sandbox.json` at the repo root, or an explicit `--sandbox <path>` when
147
- starting the daemon. The daemon searches up for this file and uses its
148
- containing directory as the VM workspace mount.
146
+ - A runtime profile in the target team. The profile supplies provider, model,
147
+ sandbox policy, and runtime defaults. The daemon mounts the current working
148
+ directory as the VM workspace root.
149
149
 
150
- For `themoltnet`, prefer the checked-in repo `sandbox.json` as-is — it carries
151
- the current pnpm/VFS workaround. A minimal `sandbox.json` for another repo:
150
+ For `themoltnet`, prefer a profile sandbox equivalent to this minimal policy:
152
151
 
153
152
  ```json
154
153
  {
@@ -181,9 +180,9 @@ workspace shape in `input.execution.workspace`: `none` becomes a
181
180
  `scratch_mount`, `shared_mount` uses the daemon mount, and
182
181
  `dedicated_worktree` uses an isolated checkout. Downstream
183
182
  `judge_eval_attempt` tasks only resolve against a still-live producer
184
- session/workspace slot. If the producer slot has already expired and been
185
- reaped, the judge fails with `producer_context_missing`. When the judge does
186
- claim in time, it immediately forks the producer session and copies the
183
+ session/workspace slot. If the producer slot or its local session/workspace
184
+ files cannot be resolved, the judge fails with `producer_context_missing`.
185
+ When the judge does claim in time, it immediately forks the producer session and copies the
187
186
  producer workspace into judge-owned scratch state so the running judge no
188
187
  longer depends on the producer slot after claim time. Repo-specific
189
188
  `resumeCommands` that should not run in scratch mode must still be guarded
@@ -251,18 +250,14 @@ pnpm --filter @themoltnet/agent-daemon dev poll \
251
250
  --agent local-dev \
252
251
  --team "$MOLTNET_TEAM_ID" \
253
252
  --task-types fulfill_brief \
254
- --provider openai-codex \
255
- --model gpt-5.4-codex \
253
+ --profile "$MOLTNET_AGENT_PROFILE" \
256
254
  --debug
257
255
  ```
258
256
 
259
- - If you're starting from a directory without `sandbox.json` at or above it,
260
- pass `--sandbox <repo-root>/sandbox.json`.
261
257
  - `--task-types fulfill_brief` scopes the queue. Omit to accept any
262
258
  registered type.
263
- - Pick provider/model that matches your pi auth credits. Common choices:
264
- `--provider openai-codex --model gpt-5.4-codex`, or
265
- `--provider anthropic --model claude-sonnet-4-6`.
259
+ - Pick a runtime profile whose provider/model matches your Pi auth credits.
260
+ Set `MOLTNET_AGENT_PROFILE` to the profile UUID or team-scoped profile name.
266
261
  - `dev` (= `tsx watch src/main.ts`) is fine for local. Use `cli` for a
267
262
  one-shot run without watch.
268
263
 
@@ -333,8 +328,7 @@ pnpm --filter @themoltnet/agent-daemon dev poll \
333
328
  --agent local-dev \
334
329
  --team "$MOLTNET_TEAM_ID" \
335
330
  --task-types pr_review \
336
- --provider openai-codex \
337
- --model gpt-5.4-codex \
331
+ --profile "$MOLTNET_AGENT_PROFILE" \
338
332
  --debug
339
333
  ```
340
334
 
@@ -396,7 +390,7 @@ snapshot. The cheap parts of the runtime contract (prompt assembly, tool-side
396
390
  tests in `libs/pi-extension`. This flow exists for the parts unit tests can't
397
391
  reach: real LLM behaviour against the assembled system prompt, real VM, real
398
392
  API round-trips, and the interaction between `.moltnet/<agent>/` identity
399
- material and the active `sandbox.json`.
393
+ material and the selected runtime profile.
400
394
 
401
395
  ## License
402
396
 
package/dist/main.js CHANGED
@@ -9,11 +9,10 @@ import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
9
9
  import crypto from "crypto";
10
10
  import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
11
11
  import { parseArgs, promisify } from "node:util";
12
- import { DaemonSlotRegistry, resolveDaemonStateStorageConfig, resolveLatestPiSessionPath } from "@themoltnet/agent-daemon-state";
13
12
  import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource } from "@themoltnet/agent-runtime";
14
13
  import { createPiTaskExecutor, findMainWorktree } from "@themoltnet/pi-extension";
15
14
  import { execFile, execFileSync } from "node:child_process";
16
- import { accessSync, constants, existsSync, mkdirSync, readFileSync } from "node:fs";
15
+ import { accessSync, constants, existsSync, mkdirSync, readdirSync } from "node:fs";
17
16
  import { MoltNetError, connect } from "@themoltnet/sdk";
18
17
  import { once } from "node:events";
19
18
  import { pino, transport } from "pino";
@@ -22,6 +21,7 @@ import { resourceFromAttributes } from "@opentelemetry/resources";
22
21
  import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
23
22
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
24
23
  import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
24
+ import { AsyncLocalStorage } from "node:async_hooks";
25
25
  //#region ../../libs/observability/src/instrumentation.ts
26
26
  /**
27
27
  * Register OTel auto-instrumentation for common Node.js modules.
@@ -4547,6 +4547,118 @@ _Object_({
4547
4547
  additionalProperties: false
4548
4548
  });
4549
4549
  //#endregion
4550
+ //#region ../../libs/tasks/src/runtime-slots.ts
4551
+ var RuntimeWorkspaceKind = Union([
4552
+ Literal("origin"),
4553
+ Literal("fork"),
4554
+ Literal("scratch")
4555
+ ]);
4556
+ var RuntimeSlotState = Union([Literal("active"), Literal("idle")]);
4557
+ var RuntimeWorkspace = _Object_({
4558
+ id: String$1({ format: "uuid" }),
4559
+ teamId: String$1({ format: "uuid" }),
4560
+ workspaceId: String$1({ minLength: 1 }),
4561
+ worktreePath: String$1({ minLength: 1 }),
4562
+ worktreeBranch: Union([String$1({ minLength: 1 }), Null()]),
4563
+ kind: RuntimeWorkspaceKind,
4564
+ createdAtMs: Integer({ minimum: 0 }),
4565
+ lastUsedAtMs: Integer({ minimum: 0 })
4566
+ }, { $id: "RuntimeWorkspace" });
4567
+ _Object_({
4568
+ slot: _Object_({
4569
+ id: String$1({ format: "uuid" }),
4570
+ teamId: String$1({ format: "uuid" }),
4571
+ agentName: String$1({
4572
+ minLength: 1,
4573
+ maxLength: 100
4574
+ }),
4575
+ runtimeProfileId: Union([String$1({ format: "uuid" }), Null()]),
4576
+ provider: String$1({
4577
+ minLength: 1,
4578
+ maxLength: 100
4579
+ }),
4580
+ model: String$1({
4581
+ minLength: 1,
4582
+ maxLength: 200
4583
+ }),
4584
+ slotKey: String$1({ minLength: 1 }),
4585
+ taskType: String$1({
4586
+ minLength: 1,
4587
+ maxLength: 100
4588
+ }),
4589
+ state: RuntimeSlotState,
4590
+ lastTaskId: String$1({ format: "uuid" }),
4591
+ lastAttemptN: Integer({ minimum: 1 }),
4592
+ sessionDir: Union([String$1({ minLength: 1 }), Null()]),
4593
+ sessionPath: Union([String$1({ minLength: 1 }), Null()]),
4594
+ workspaceRowId: Union([String$1({ format: "uuid" }), Null()]),
4595
+ createdAtMs: Integer({ minimum: 0 }),
4596
+ lastUsedAtMs: Integer({ minimum: 0 }),
4597
+ expiresAtMs: Integer({ minimum: 0 })
4598
+ }, { $id: "RuntimeSlot" }),
4599
+ workspace: Union([RuntimeWorkspace, Null()])
4600
+ }, { $id: "ResolvedRuntimeSlot" });
4601
+ _Object_({
4602
+ agentName: String$1({
4603
+ minLength: 1,
4604
+ maxLength: 100
4605
+ }),
4606
+ runtimeProfileId: String$1({ format: "uuid" }),
4607
+ provider: String$1({
4608
+ minLength: 1,
4609
+ maxLength: 100
4610
+ }),
4611
+ model: String$1({
4612
+ minLength: 1,
4613
+ maxLength: 200
4614
+ }),
4615
+ slotKey: String$1({ minLength: 1 }),
4616
+ taskType: String$1({
4617
+ minLength: 1,
4618
+ maxLength: 100
4619
+ }),
4620
+ sessionDir: Optional(String$1({ minLength: 1 })),
4621
+ sessionPath: Optional(String$1({ minLength: 1 })),
4622
+ workspaceId: Optional(String$1({ minLength: 1 })),
4623
+ worktreePath: Optional(String$1({ minLength: 1 })),
4624
+ worktreeBranch: Optional(String$1({ minLength: 1 })),
4625
+ workspaceKind: Optional(RuntimeWorkspaceKind),
4626
+ lastTaskId: String$1({ format: "uuid" }),
4627
+ lastAttemptN: Integer({ minimum: 1 })
4628
+ }, {
4629
+ $id: "BeginRuntimeSlotBody",
4630
+ additionalProperties: false
4631
+ });
4632
+ _Object_({
4633
+ agentName: String$1({
4634
+ minLength: 1,
4635
+ maxLength: 100
4636
+ }),
4637
+ runtimeProfileId: String$1({ format: "uuid" }),
4638
+ provider: String$1({
4639
+ minLength: 1,
4640
+ maxLength: 100
4641
+ }),
4642
+ model: String$1({
4643
+ minLength: 1,
4644
+ maxLength: 200
4645
+ }),
4646
+ slotKey: String$1({ minLength: 1 }),
4647
+ taskId: String$1({ format: "uuid" }),
4648
+ attemptN: Integer({ minimum: 1 }),
4649
+ sessionPath: Optional(String$1({ minLength: 1 }))
4650
+ }, {
4651
+ $id: "FinishRuntimeSlotBody",
4652
+ additionalProperties: false
4653
+ });
4654
+ _Object_({
4655
+ taskId: String$1({ format: "uuid" }),
4656
+ attemptN: Integer({ minimum: 1 })
4657
+ }, {
4658
+ $id: "FindLatestRuntimeSlotForAttemptQuery",
4659
+ additionalProperties: false
4660
+ });
4661
+ //#endregion
4550
4662
  //#region ../../libs/tasks/src/success-criteria.ts
4551
4663
  /**
4552
4664
  * SuccessCriteria — proposer-stated acceptance criteria, evaluated in two
@@ -9011,23 +9123,14 @@ _Object_({
9011
9123
  //#region src/lib/help.ts
9012
9124
  var COMMON_REQUIRED_FLAGS = `\
9013
9125
  -a, --agent <name> MoltNet agent identity. Reads credentials
9014
- from <repo-root>/.moltnet/<name>/moltnet.json.`;
9015
- var COMMON_MODEL_FLAGS = `\
9016
- -p, --provider <id> LLM provider id (e.g. anthropic, openai-codex).
9017
- -m, --model <id> LLM model id for the provider (e.g.
9018
- claude-sonnet-4-5, gpt-5.3-codex). Required
9019
- unless --profile is set.`;
9126
+ from <repo-root>/.moltnet/<name>/moltnet.json.
9127
+ --profile <uuid|name> Remote runtime profile. Repeat for poll/drain
9128
+ to declare priority order. Provider, model,
9129
+ sandbox policy, prerequisites, and runtime
9130
+ defaults come from the selected profile.`;
9020
9131
  var COMMON_OPTIONAL_FLAGS = `\
9021
- --sandbox <path> Path to sandbox.json. Default: search up from
9022
- the daemon's CWD until found. The directory
9023
- containing sandbox.json is also used as the
9024
- VM mountPath. Cannot be used with --profile.
9025
- --profile <uuid|name> Remote runtime profile. When set, provider,
9026
- model, and sandbox policy come from the
9027
- profile; task listing/claiming is restricted
9028
- to unrestricted tasks plus tasks allowing this
9029
- profile. requiredEnv/requiredTools are checked
9030
- before claiming. Name lookup is team-scoped.
9132
+ --sandbox <path> Deprecated. Remote runtime profiles define
9133
+ sandbox policy.
9031
9134
  --lease-ttl-sec <n> Sliding liveness window. Silence longer than
9032
9135
  this ends the attempt with lease_expired.
9033
9136
  Default: 300.
@@ -9042,9 +9145,8 @@ var COMMON_OPTIONAL_FLAGS = `\
9042
9145
  this, the pi session aborts and the attempt
9043
9146
  fails with code 'max_bash_timeouts_exceeded'.
9044
9147
  0 = disabled. Default: 3.
9045
- --warm-session-ttl-sec <n> Retain resumable daemon slots (Pi sessions +
9046
- reusable worktrees) in local daemon state for
9047
- this many seconds after use. 0 = disable reuse.
9148
+ --warm-session-ttl-sec <n> Resumability window for runtime slots
9149
+ (Pi sessions + reusable worktrees) after use.
9048
9150
  Default: 1800, or min(profile session/workspace
9049
9151
  TTL) when --profile is set.
9050
9152
  --debug Verbose logging: also log successful list/claim
@@ -9069,23 +9171,20 @@ Run \`agent-daemon <command> --help\` for command-specific flags.
9069
9171
 
9070
9172
  Prerequisites (all subcommands):
9071
9173
  - <repo-root>/.moltnet/<agent>/moltnet.json — credentials (see --agent)
9072
- - sandbox.json or --profile — local sandbox config is resolved by
9073
- searching up from CWD, or pass --sandbox <path>. With --profile, the
9074
- remote runtime profile supplies provider/model/sandbox policy and CWD
9075
- is used as the VM mountPath.
9174
+ - --profile — remote runtime profile supplies provider/model/sandbox
9175
+ policy and CWD is used as the VM mountPath.
9076
9176
 
9077
9177
  Registered task types: ${knownTaskTypesList()}`;
9078
9178
  var POLL_HELP = `\
9079
9179
  agent-daemon poll — long-running task worker.
9080
9180
 
9081
9181
  Usage:
9082
- agent-daemon poll --team <uuid> --agent <name> --provider <p> --model <m> [...]
9182
+ agent-daemon poll --team <uuid> --agent <name> --profile <uuid|name> [...]
9083
9183
 
9084
9184
  Required:
9085
9185
  --team <uuid> Team whose queue to serve. The daemon must be
9086
9186
  a member of this team (canAccessTeam permit).
9087
9187
  ${COMMON_REQUIRED_FLAGS}
9088
- ${COMMON_MODEL_FLAGS}
9089
9188
 
9090
9189
  Optional:
9091
9190
  --task-types <csv> Whitelist of task types to claim. Default:
@@ -9102,38 +9201,38 @@ Example:
9102
9201
  --team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
9103
9202
  --task-types curate_pack,fulfill_brief \\
9104
9203
  --agent legreffier \\
9105
- --provider anthropic \\
9106
- --model claude-sonnet-4-5
9204
+ --profile github-linear \\
9205
+ --profile local-fallback
9107
9206
 
9108
9207
  Stops cleanly on SIGINT/SIGTERM (drains the in-flight task before exit).`;
9109
9208
  var ONCE_HELP = `\
9110
9209
  agent-daemon once — execute one specific queued task by id, then exit.
9111
9210
 
9112
9211
  Usage:
9113
- agent-daemon once --task-id <uuid> --agent <name> --provider <p> --model <m> [...]
9212
+ agent-daemon once --task-id <uuid> --agent <name> --profile <uuid|name> [...]
9114
9213
 
9115
9214
  Required:
9116
9215
  -t, --task-id <uuid> Task to claim and execute. Must already be
9117
9216
  in 'queued' status.
9118
9217
  ${COMMON_REQUIRED_FLAGS}
9119
- ${COMMON_MODEL_FLAGS}
9120
9218
 
9121
9219
  Optional:
9220
+ --team <uuid> Team scope for resolving --profile by name.
9221
+ Required only when --profile is a name.
9122
9222
  ${COMMON_OPTIONAL_FLAGS}
9123
9223
 
9124
9224
  Example:
9125
9225
  agent-daemon once \\
9126
9226
  --task-id 26004a77-bc10-43ef-a79f-c8e62faf59b1 \\
9127
9227
  --agent legreffier \\
9128
- --provider anthropic \\
9129
- --model claude-sonnet-4-5
9228
+ --profile github-linear
9130
9229
 
9131
9230
  Exits 0 on completed, 1 on failed/cancelled/runtime-error.`;
9132
9231
  var DRAIN_HELP = `\
9133
9232
  agent-daemon drain — poll until the queue is empty, then exit.
9134
9233
 
9135
9234
  Usage:
9136
- agent-daemon drain --team <uuid> --agent <name> --provider <p> --model <m> [...]
9235
+ agent-daemon drain --team <uuid> --agent <name> --profile <uuid|name> [...]
9137
9236
 
9138
9237
  Same flags as \`poll\`. The only behavioural difference: \`drain\` exits
9139
9238
  when a list call confirms no claimable tasks remain (vs \`poll\` which
@@ -9142,7 +9241,6 @@ sleeps and retries forever).
9142
9241
  Required:
9143
9242
  --team <uuid> Team whose queue to drain.
9144
9243
  ${COMMON_REQUIRED_FLAGS}
9145
- ${COMMON_MODEL_FLAGS}
9146
9244
 
9147
9245
  Optional:
9148
9246
  --task-types <csv> Whitelist. Known types: ${knownTaskTypesList()}
@@ -9157,8 +9255,7 @@ Example:
9157
9255
  --team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
9158
9256
  --task-types judge_pack \\
9159
9257
  --agent legreffier \\
9160
- --provider anthropic \\
9161
- --model claude-sonnet-4-5`;
9258
+ --profile eval-judge`;
9162
9259
  function isHelpFlag(args) {
9163
9260
  return args.includes("--help") || args.includes("-h");
9164
9261
  }
@@ -9166,7 +9263,6 @@ function isHelpFlag(args) {
9166
9263
  //#region src/config.ts
9167
9264
  function loadConfig() {
9168
9265
  return {
9169
- agentDaemonStateDatabaseUrl: process.env["MOLTNET_AGENT_DAEMON_STATE_DATABASE_URL"] ?? "",
9170
9266
  otelEndpoint: process.env["MOLTNET_OTEL_ENDPOINT"] ?? "",
9171
9267
  logLevel: process.env["LOG_LEVEL"] ?? "",
9172
9268
  profilePrerequisiteEnv: process.env,
@@ -9265,6 +9361,16 @@ function createGhCliClient() {
9265
9361
  };
9266
9362
  }
9267
9363
  //#endregion
9364
+ //#region src/lib/session-files.ts
9365
+ function resolveLatestPiSessionPath(sessionDir) {
9366
+ try {
9367
+ const latestEntry = readdirSync(sessionDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => entry.name).sort().at(-1);
9368
+ return latestEntry ? join(sessionDir, latestEntry) : null;
9369
+ } catch {
9370
+ return null;
9371
+ }
9372
+ }
9373
+ //#endregion
9268
9374
  //#region src/lib/slugify.ts
9269
9375
  function slugifyAsciiLower(input, maxLen, preserveChars = []) {
9270
9376
  const preserved = new Set(preserveChars.map((char) => char.toLowerCase()));
@@ -9354,10 +9460,8 @@ function buildDaemonSlotId(identity, slotKey) {
9354
9460
  return [
9355
9461
  "agent",
9356
9462
  slugSlotIdentityComponent(identity.agentName),
9357
- "provider",
9358
- slugSlotIdentityComponent(identity.provider),
9359
- "model",
9360
- slugSlotIdentityComponent(identity.model),
9463
+ "profile",
9464
+ slugSlotIdentityComponent(identity.runtimeProfileId),
9361
9465
  "key",
9362
9466
  slotKey
9363
9467
  ].join(":");
@@ -9419,8 +9523,8 @@ function createExecutionPlanCache(args) {
9419
9523
  function buildClaimedTaskKey(task) {
9420
9524
  return `${task.task.id}:${task.attemptN}`;
9421
9525
  }
9422
- async function resolveWarmSlot(slotRegistry, sourceTaskId, sourceAttemptN, stateDirs) {
9423
- const producerContext = await slotRegistry.findLatestProducerSlotByTaskAttempt(sourceTaskId, sourceAttemptN);
9526
+ async function resolveWarmSlot(slotRegistry, teamId, sourceTaskId, sourceAttemptN, stateDirs) {
9527
+ const producerContext = await slotRegistry.findLatestSlotByTaskAttempt(teamId, sourceTaskId, sourceAttemptN);
9424
9528
  if (!producerContext) return { kind: "missing" };
9425
9529
  const sourceSessionPath = resolveProducerSessionPath(producerContext);
9426
9530
  if (!sourceSessionPath) return { kind: "no-session-path" };
@@ -9435,8 +9539,8 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
9435
9539
  if (claimedTask.task.taskType === "freeform") {
9436
9540
  const continueFrom = claimedTask.task.input.continueFrom;
9437
9541
  if (!continueFrom) return basePlan;
9438
- const resolution = await resolveWarmSlot(slotRegistry, continueFrom.taskId, continueFrom.attemptN, stateDirs);
9439
- if (resolution.kind === "missing") throw new ProducerContextResolutionError(`Continuation source task ${continueFrom.taskId} attempt ${continueFrom.attemptN} has no live daemon slot on this daemon — claim affinity filter should have prevented this claim`);
9542
+ const resolution = await resolveWarmSlot(slotRegistry, claimedTask.task.teamId, continueFrom.taskId, continueFrom.attemptN, stateDirs);
9543
+ if (resolution.kind === "missing") throw new ProducerContextResolutionError(`Continuation source task ${continueFrom.taskId} attempt ${continueFrom.attemptN} has no live runtime slot on this daemon — claim affinity filter should have prevented this claim`);
9440
9544
  if (resolution.kind === "no-session-path") throw new ProducerContextResolutionError(`Continuation source attempt ${continueFrom.taskId}/${continueFrom.attemptN} has no persisted Pi session path`);
9441
9545
  const sessionDir = `${stateDirs.piSessionsDir}/continue-${claimedTask.task.id}-attempt-${claimedTask.attemptN}`;
9442
9546
  const parentBranch = resolution.producerSlot.workspace?.worktreeBranch ?? null;
@@ -9472,8 +9576,8 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
9472
9576
  const targetTaskId = typeof claimedTask.task.input.targetTaskId === "string" ? claimedTask.task.input.targetTaskId : null;
9473
9577
  const targetAttemptN = typeof claimedTask.task.input.targetAttemptN === "number" ? claimedTask.task.input.targetAttemptN : null;
9474
9578
  if (!targetTaskId || !targetAttemptN) throw new ProducerContextResolutionError("judge_eval_attempt is missing targetTaskId/targetAttemptN");
9475
- const resolution = await resolveWarmSlot(slotRegistry, targetTaskId, targetAttemptN, stateDirs);
9476
- if (resolution.kind === "missing") throw new ProducerContextResolutionError(`No live producer daemon slot found for task ${targetTaskId} attempt ${targetAttemptN}`);
9579
+ const resolution = await resolveWarmSlot(slotRegistry, claimedTask.task.teamId, targetTaskId, targetAttemptN, stateDirs);
9580
+ if (resolution.kind === "missing") throw new ProducerContextResolutionError(`No live producer runtime slot found for task ${targetTaskId} attempt ${targetAttemptN}`);
9477
9581
  if (resolution.kind === "no-session-path") throw new ProducerContextResolutionError(`Producer task ${targetTaskId} attempt ${targetAttemptN} has no persisted Pi session path`);
9478
9582
  return {
9479
9583
  ...basePlan,
@@ -9520,7 +9624,7 @@ function recoverScratchWorkspacePath(producer, stateDirs) {
9520
9624
  //#region src/lib/finalize.ts
9521
9625
  /**
9522
9626
  * Build the `daemonState` payload for a `/complete` call. Only freeform
9523
- * attempts that ran with a warm slot are eligible for continuation
9627
+ * attempts that ran with a runtime slot are eligible for continuation
9524
9628
  * (`tasks_continue`, see issue #1287). Other task types and slot-less
9525
9629
  * freeform completions report `null` for `slotResumableUntil`, which
9526
9630
  * the server persists verbatim — continuations against such attempts
@@ -9672,7 +9776,6 @@ var MissingRequiredOptionError = class extends Error {
9672
9776
  }
9673
9777
  };
9674
9778
  function parseCommonOptions(args, options = {}) {
9675
- const requireProviderModel = options.requireProviderModel ?? true;
9676
9779
  const runtimeDefaults = {
9677
9780
  leaseTtlSec: options.runtimeDefaults?.leaseTtlSec ?? DEFAULTS.leaseTtlSec,
9678
9781
  heartbeatIntervalMs: options.runtimeDefaults?.heartbeatIntervalMs ?? DEFAULTS.heartbeatIntervalMs,
@@ -9680,13 +9783,9 @@ function parseCommonOptions(args, options = {}) {
9680
9783
  warmSessionTtlSec: options.runtimeDefaults?.warmSessionTtlSec ?? DEFAULTS.warmSessionTtlSec
9681
9784
  };
9682
9785
  if (!args.agent) throw new MissingRequiredOptionError("agent");
9683
- if (requireProviderModel && !args.provider) throw new MissingRequiredOptionError("provider");
9684
- if (requireProviderModel && !args.model) throw new MissingRequiredOptionError("model");
9685
9786
  if (!/^[a-zA-Z0-9_-]+$/.test(args.agent)) throw new Error(`Invalid --agent "${args.agent}": must match /^[a-zA-Z0-9_-]+$/`);
9686
9787
  return {
9687
9788
  agent: args.agent,
9688
- ...args.provider ? { provider: args.provider } : {},
9689
- ...args.model ? { model: args.model } : {},
9690
9789
  leaseTtlSec: parsePositiveInt(args["lease-ttl-sec"], "lease-ttl-sec", runtimeDefaults.leaseTtlSec),
9691
9790
  heartbeatIntervalMs: parseNonNegativeInt(args["heartbeat-interval-ms"], "heartbeat-interval-ms", runtimeDefaults.heartbeatIntervalMs),
9692
9791
  maxBatchSize: parsePositiveInt(args["max-batch-size"], "max-batch-size", runtimeDefaults.maxBatchSize),
@@ -9715,14 +9814,6 @@ function commonOptionDefs() {
9715
9814
  type: "string",
9716
9815
  short: "a"
9717
9816
  },
9718
- model: {
9719
- type: "string",
9720
- short: "m"
9721
- },
9722
- provider: {
9723
- type: "string",
9724
- short: "p"
9725
- },
9726
9817
  "lease-ttl-sec": { type: "string" },
9727
9818
  "heartbeat-interval-ms": { type: "string" },
9728
9819
  "max-batch-size": { type: "string" },
@@ -9801,6 +9892,12 @@ function ensurePiAgentDir(repoRoot, explicitPath) {
9801
9892
  };
9802
9893
  }
9803
9894
  //#endregion
9895
+ //#region src/lib/runtime-context.ts
9896
+ var storage = new AsyncLocalStorage();
9897
+ function runWithDaemonRuntimeContext(context, callback) {
9898
+ return storage.run(context, callback);
9899
+ }
9900
+ //#endregion
9804
9901
  //#region src/lib/runtime-profile.ts
9805
9902
  var RuntimeProfilePrerequisiteError = class extends Error {
9806
9903
  constructor(profileName, missingEnv, missingTools) {
@@ -9834,6 +9931,22 @@ async function resolveRuntimeProfile(options) {
9834
9931
  source: `runtime-profile:${profile.id}`
9835
9932
  };
9836
9933
  }
9934
+ async function resolveRuntimeProfiles(options) {
9935
+ const seen = /* @__PURE__ */ new Set();
9936
+ const out = [];
9937
+ for (const profile of options.profiles) {
9938
+ const resolved = await resolveRuntimeProfile({
9939
+ agent: options.agent,
9940
+ profile,
9941
+ teamId: options.teamId,
9942
+ cwd: options.cwd
9943
+ });
9944
+ if (seen.has(resolved.id)) continue;
9945
+ seen.add(resolved.id);
9946
+ out.push(resolved);
9947
+ }
9948
+ return out;
9949
+ }
9837
9950
  function validateRuntimeProfilePrerequisites(profile, env, pathValue) {
9838
9951
  const missingEnv = profile.requiredEnv.filter((name) => !env[name]);
9839
9952
  const missingTools = profile.requiredTools.filter((tool) => !isExecutableOnPath(tool, pathValue));
@@ -9866,33 +9979,63 @@ function isExecutable(path) {
9866
9979
  }
9867
9980
  }
9868
9981
  //#endregion
9869
- //#region src/lib/sandbox.ts
9870
- function resolveSandbox(startDir, explicitPath) {
9871
- const path = explicitPath ? isAbsolute(explicitPath) ? explicitPath : resolve(startDir, explicitPath) : findUp(startDir, "sandbox.json");
9872
- if (!path) throw new Error(`sandbox.json not found in ${startDir} or any parent directory. Pass --sandbox <path> or run the daemon from a directory with sandbox.json at or above it.`);
9873
- let config;
9874
- try {
9875
- config = JSON.parse(readFileSync(path, "utf8"));
9876
- } catch (err) {
9877
- const isEnoent = err instanceof Error && "code" in err && err.code === "ENOENT";
9878
- throw new Error(isEnoent ? `sandbox.json not found at ${path}.` : `Failed to read sandbox.json at ${path}: ${err instanceof Error ? err.message : String(err)}`);
9879
- }
9982
+ //#region src/lib/runtime-slots.ts
9983
+ function createApiRuntimeSlotStore(args) {
9984
+ const { agent } = args;
9880
9985
  return {
9881
- config,
9882
- rootDir: dirname(path),
9883
- path
9986
+ async beginSlot(input) {
9987
+ await agent.runtimeSlots.begin({
9988
+ agentName: input.agentName,
9989
+ runtimeProfileId: input.runtimeProfileId,
9990
+ lastAttemptN: input.lastAttemptN,
9991
+ lastTaskId: input.lastTaskId,
9992
+ model: input.model,
9993
+ provider: input.provider,
9994
+ sessionDir: input.sessionDir ?? void 0,
9995
+ sessionPath: input.sessionPath ?? void 0,
9996
+ slotKey: input.slotKey,
9997
+ taskType: input.taskType,
9998
+ workspaceId: input.workspaceId ?? void 0,
9999
+ workspaceKind: input.workspaceKind,
10000
+ worktreeBranch: input.worktreeBranch ?? void 0,
10001
+ worktreePath: input.worktreePath ?? void 0
10002
+ }, { teamId: input.teamId });
10003
+ },
10004
+ async finishSlot(teamId, taskId, attemptN, identity, slotKey, provider, model, sessionPath) {
10005
+ await agent.runtimeSlots.finish({
10006
+ agentName: identity.agentName,
10007
+ attemptN,
10008
+ runtimeProfileId: identity.runtimeProfileId,
10009
+ model,
10010
+ provider,
10011
+ sessionPath: sessionPath ?? void 0,
10012
+ slotKey,
10013
+ taskId
10014
+ }, { teamId });
10015
+ },
10016
+ async findLatestSlotByTaskAttempt(teamId, taskId, attemptN) {
10017
+ const resolved = await agent.runtimeSlots.findLatestForAttempt({
10018
+ attemptN,
10019
+ taskId
10020
+ }, { teamId });
10021
+ if (!resolved) return null;
10022
+ return {
10023
+ slot: { expiresAtMs: resolved.slot.expiresAtMs },
10024
+ session: resolved.slot.sessionDir ? {
10025
+ sessionDir: resolved.slot.sessionDir,
10026
+ sessionPath: resolved.slot.sessionPath
10027
+ } : null,
10028
+ workspace: resolved.workspace ? {
10029
+ kind: resolved.workspace.kind,
10030
+ workspaceId: resolved.workspace.workspaceId,
10031
+ worktreeBranch: resolved.workspace.worktreeBranch,
10032
+ worktreePath: resolved.workspace.worktreePath
10033
+ } : null
10034
+ };
10035
+ },
10036
+ async close() {}
9884
10037
  };
9885
10038
  }
9886
- function findUp(startDir, filename) {
9887
- let dir = resolve(startDir);
9888
- while (true) {
9889
- const candidate = resolve(dir, filename);
9890
- if (existsSync(candidate)) return candidate;
9891
- const parent = dirname(dir);
9892
- if (parent === dir) return null;
9893
- dir = parent;
9894
- }
9895
- }
9896
10039
  //#endregion
9897
10040
  //#region src/lib/shutdown-signal.ts
9898
10041
  function installShutdownSignalHandlers(opts) {
@@ -9933,8 +10076,7 @@ function ensureDaemonStateDirs(mountPath) {
9933
10076
  mkdirSync(piSessionsDir, { recursive: true });
9934
10077
  return {
9935
10078
  rootDir,
9936
- piSessionsDir,
9937
- registryDbPath: join(rootDir, "daemon-state.sqlite")
10079
+ piSessionsDir
9938
10080
  };
9939
10081
  }
9940
10082
  //#endregion
@@ -9976,7 +10118,10 @@ async function runPolling(opts) {
9976
10118
  "max-poll-interval-ms": { type: "string" },
9977
10119
  "list-limit": { type: "string" },
9978
10120
  sandbox: { type: "string" },
9979
- profile: { type: "string" }
10121
+ profile: {
10122
+ type: "string",
10123
+ multiple: true
10124
+ }
9980
10125
  }
9981
10126
  });
9982
10127
  if (!values.team) {
@@ -9985,6 +10130,12 @@ async function runPolling(opts) {
9985
10130
  return 1;
9986
10131
  }
9987
10132
  const teamId = values.team;
10133
+ const profileValues = parseProfileValues(values.profile);
10134
+ if (profileValues.length === 0) {
10135
+ console.error("Missing required flag: --profile\n");
10136
+ console.error(opts.helpText);
10137
+ return 1;
10138
+ }
9988
10139
  let taskTypes;
9989
10140
  try {
9990
10141
  taskTypes = validateTaskTypes(parseCsv(values["task-types"]));
@@ -9994,9 +10145,9 @@ async function runPolling(opts) {
9994
10145
  return 1;
9995
10146
  }
9996
10147
  const diaryIds = parseCsv(values["diary-ids"]);
9997
- let common;
10148
+ let baseCommon;
9998
10149
  try {
9999
- common = parseCommonOptions(values, { requireProviderModel: !values.profile });
10150
+ baseCommon = parseCommonOptions(values);
10000
10151
  } catch (err) {
10001
10152
  if (err instanceof MissingRequiredOptionError) {
10002
10153
  console.error(`${err.message}\n`);
@@ -10008,81 +10159,82 @@ async function runPolling(opts) {
10008
10159
  const pollIntervalMs = optionalPositiveInt(values["poll-interval-ms"], "poll-interval-ms", 2e3);
10009
10160
  const maxPollIntervalMs = optionalPositiveInt(values["max-poll-interval-ms"], "max-poll-interval-ms", 3e4);
10010
10161
  const listLimit = optionalPositiveInt(values["list-limit"], "list-limit", 10);
10011
- if (values.profile && values.sandbox) {
10012
- console.error(`[${opts.modeLabel}] Cannot use --sandbox with --profile. Remote runtime profiles define sandbox policy.`);
10162
+ if (values.sandbox) {
10163
+ console.error(`[${opts.modeLabel}] Cannot use --sandbox. Remote runtime profiles define sandbox policy.`);
10013
10164
  return 1;
10014
10165
  }
10015
10166
  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).`);
10016
10167
  const cfg = loadConfig();
10017
- const ctx = await resolveAgentContext(common.agent);
10018
- const profile = values.profile ? await resolveRuntimeProfile({
10168
+ const ctx = await resolveAgentContext(baseCommon.agent);
10169
+ const profiles = await resolveRuntimeProfiles({
10019
10170
  agent: ctx.agent,
10020
- profile: values.profile,
10171
+ profiles: profileValues,
10021
10172
  teamId,
10022
10173
  cwd: process.cwd()
10023
- }) : null;
10024
- if (profile) {
10025
- validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
10026
- common = parseCommonOptions(values, {
10027
- requireProviderModel: false,
10028
- runtimeDefaults: {
10029
- leaseTtlSec: profile.leaseTtlSec,
10030
- heartbeatIntervalMs: profile.heartbeatIntervalMs,
10031
- maxBatchSize: profile.maxBatchSize,
10032
- warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
10033
- }
10174
+ });
10175
+ for (const profile of profiles) validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
10176
+ const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
10177
+ const mainRepo = findMainWorktree();
10178
+ const runtimes = /* @__PURE__ */ new Map();
10179
+ for (const profile of profiles) {
10180
+ const common = parseCommonOptions(values, { runtimeDefaults: {
10181
+ leaseTtlSec: profile.leaseTtlSec,
10182
+ heartbeatIntervalMs: profile.heartbeatIntervalMs,
10183
+ maxBatchSize: profile.maxBatchSize,
10184
+ warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
10185
+ } });
10186
+ const sandbox = {
10187
+ config: profile.sandboxConfig,
10188
+ rootDir: profile.mountPath,
10189
+ path: profile.source
10190
+ };
10191
+ const piAgentDir = ensurePiAgentDir(sandbox.rootDir, cfg.piCodingAgentDir);
10192
+ const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
10193
+ const slotIdentity = {
10194
+ agentName: common.agent,
10195
+ runtimeProfileId: profile.id
10196
+ };
10197
+ const executionPlans = createExecutionPlanCache({
10198
+ stateDirs,
10199
+ slotIdentity,
10200
+ warmSessionTtlSec: common.warmSessionTtlSec,
10201
+ slotRegistry
10202
+ });
10203
+ runtimes.set(profile.id, {
10204
+ common,
10205
+ profile,
10206
+ sandbox,
10207
+ stateDirs,
10208
+ piAgentDir,
10209
+ slotIdentity,
10210
+ executionPlans
10034
10211
  });
10035
10212
  }
10036
- const provider = profile?.provider ?? common.provider;
10037
- const model = profile?.model ?? common.model;
10038
- if (!provider || !model) throw new Error("provider/model missing after runtime profile resolution");
10039
- const sandbox = profile ? {
10040
- config: profile.sandboxConfig,
10041
- rootDir: profile.mountPath,
10042
- path: profile.source
10043
- } : resolveSandbox(process.cwd(), values.sandbox);
10044
- const piAgentDir = ensurePiAgentDir(sandbox.rootDir, cfg.piCodingAgentDir);
10213
+ const firstRuntime = runtimes.get(profiles[0].id);
10214
+ if (!firstRuntime) throw new Error("No runtime profiles resolved");
10215
+ const piAgentDir = firstRuntime.piAgentDir;
10045
10216
  activatePiCodingAgentDir(piAgentDir.path);
10046
- const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
10047
- const slotRegistry = new DaemonSlotRegistry(resolveDaemonStateStorageConfig(stateDirs.registryDbPath, cfg.agentDaemonStateDatabaseUrl));
10048
- const slotIdentity = {
10049
- agentName: common.agent,
10050
- provider,
10051
- model
10052
- };
10053
- const mainRepo = findMainWorktree();
10054
- const executionPlans = createExecutionPlanCache({
10055
- stateDirs,
10056
- slotIdentity,
10057
- warmSessionTtlSec: common.warmSessionTtlSec,
10058
- slotRegistry
10059
- });
10060
10217
  const otelShutdown = await initWorkerOtel({
10061
10218
  serviceName: opts.serviceName,
10062
10219
  agentDir: ctx.agentDir,
10063
10220
  endpoint: cfg.otelEndpoint,
10064
10221
  resourceAttributes: {
10065
10222
  "moltnet.team.id": teamId,
10066
- "moltnet.agent.name": common.agent,
10067
- "moltnet.llm.provider": provider,
10068
- "moltnet.llm.model": model,
10069
- ...profile ? { "moltnet.daemon_profile.id": profile.id } : {}
10223
+ "moltnet.agent.name": baseCommon.agent,
10224
+ "moltnet.runtime_profile.count": String(profiles.length),
10225
+ "moltnet.runtime_profile.ids": profiles.map((p) => p.id).join(",")
10070
10226
  }
10071
10227
  });
10072
10228
  const { logger, shutdown: shutdownLogger } = createRootLogger({
10073
10229
  name: `agent-daemon.${opts.modeLabel}`,
10074
- level: cfg.logLevel || (common.debug ? "debug" : "info")
10230
+ level: cfg.logLevel || (baseCommon.debug ? "debug" : "info")
10075
10231
  });
10076
10232
  const rootLogger = logger.child({
10077
10233
  mode: opts.modeLabel,
10078
- agent: common.agent,
10234
+ agent: baseCommon.agent,
10079
10235
  teamId,
10080
- provider,
10081
- model,
10082
- ...profile ? {
10083
- daemonProfileId: profile.id,
10084
- daemonProfileName: profile.name
10085
- } : {}
10236
+ runtimeProfileIds: profiles.map((p) => p.id),
10237
+ runtimeProfileNames: profiles.map((p) => p.name)
10086
10238
  });
10087
10239
  const abort = new AbortController();
10088
10240
  let runtime = null;
@@ -10108,90 +10260,91 @@ async function runPolling(opts) {
10108
10260
  }
10109
10261
  });
10110
10262
  rootLogger.info({
10111
- sandbox: sandbox.path,
10112
10263
  taskTypes: taskTypes.length > 0 ? taskTypes : ["*"],
10113
10264
  diaryIds: diaryIds.length > 0 ? diaryIds : ["*"],
10114
- leaseTtlSec: common.leaseTtlSec,
10115
- heartbeatIntervalMs: common.heartbeatIntervalMs,
10116
- warmSessionTtlSec: common.warmSessionTtlSec,
10117
10265
  pollIntervalMs,
10118
10266
  maxPollIntervalMs,
10119
- ...profile ? {
10120
- profileId: profile.id,
10121
- profileSessionTtlSec: profile.sessionTtlSec,
10122
- profileWorkspaceTtlSec: profile.workspaceTtlSec
10123
- } : {},
10267
+ profiles: profiles.map((profile) => {
10268
+ const runtime = requireRuntime(runtimes, profile.id);
10269
+ return {
10270
+ id: profile.id,
10271
+ name: profile.name,
10272
+ provider: profile.provider,
10273
+ model: profile.model,
10274
+ sandbox: runtime.sandbox.path,
10275
+ leaseTtlSec: runtime.common.leaseTtlSec,
10276
+ heartbeatIntervalMs: runtime.common.heartbeatIntervalMs,
10277
+ warmSessionTtlSec: runtime.common.warmSessionTtlSec,
10278
+ profileSessionTtlSec: profile.sessionTtlSec,
10279
+ profileWorkspaceTtlSec: profile.workspaceTtlSec
10280
+ };
10281
+ }),
10124
10282
  piAgentDir: piAgentDir.path,
10125
10283
  piAgentDirSource: piAgentDir.source
10126
10284
  }, "agent-daemon.starting");
10127
10285
  const outputs = [];
10128
10286
  try {
10129
- const rawExecuteTask = createPiTaskExecutor({
10130
- agentName: common.agent,
10131
- mountPath: sandbox.rootDir,
10132
- provider,
10133
- model,
10134
- sandboxConfig: sandbox.config,
10135
- makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
10136
- makeOnTurnEvent: makeTurnEventHandlerFactory(rootLogger),
10137
- maxTurns: common.maxTurns,
10138
- maxBashTimeouts: common.maxBashTimeouts
10139
- });
10140
- const executeTask = async (claimedTask, reporter) => {
10141
- active = {
10142
- taskId: claimedTask.task.id,
10143
- attemptN: claimedTask.attemptN
10144
- };
10145
- try {
10146
- return await rawExecuteTask(claimedTask, reporter);
10147
- } finally {
10148
- active = null;
10149
- }
10150
- };
10151
10287
  runtime = new AgentRuntime({
10152
10288
  logger: rootLogger,
10153
10289
  source: new PollingApiTaskSource({
10154
10290
  agent: ctx.agent,
10155
10291
  teamId,
10156
10292
  taskTypes: taskTypes.length > 0 ? taskTypes : void 0,
10157
- ...profile ? { profileId: profile.id } : {},
10293
+ profiles: profiles.map((profile) => ({
10294
+ profileId: profile.id,
10295
+ leaseTtlSec: requireRuntime(runtimes, profile.id).common.leaseTtlSec
10296
+ })),
10158
10297
  diaryIds: diaryIds.length > 0 ? diaryIds : void 0,
10159
- leaseTtlSec: common.leaseTtlSec,
10298
+ leaseTtlSec: firstRuntime.common.leaseTtlSec,
10160
10299
  listLimit,
10161
10300
  pollIntervalMs,
10162
10301
  maxPollIntervalMs,
10163
10302
  signal: abort.signal,
10164
10303
  stopWhenEmpty: opts.stopWhenEmpty,
10165
- debug: common.debug,
10304
+ debug: baseCommon.debug,
10166
10305
  logger: rootLogger,
10167
10306
  slotRegistry
10168
10307
  }),
10169
- makeReporter: () => new ApiTaskReporter({
10170
- tasks: ctx.agent.tasks,
10171
- leaseTtlSec: common.leaseTtlSec,
10172
- heartbeatIntervalMs: common.heartbeatIntervalMs,
10173
- maxBatchSize: common.maxBatchSize,
10174
- flushIntervalMs: common.flushIntervalMs
10175
- }),
10308
+ makeReporter: (claimedTask) => {
10309
+ const selected = runtimeForClaimedTask(runtimes, claimedTask);
10310
+ return new ApiTaskReporter({
10311
+ tasks: ctx.agent.tasks,
10312
+ leaseTtlSec: selected.common.leaseTtlSec,
10313
+ heartbeatIntervalMs: selected.common.heartbeatIntervalMs,
10314
+ maxBatchSize: selected.common.maxBatchSize,
10315
+ flushIntervalMs: selected.common.flushIntervalMs
10316
+ });
10317
+ },
10176
10318
  onTaskFinished: async (output, claimedTask) => {
10177
- const resolved = await slotRegistry.findLatestProducerSlotByTaskAttempt(claimedTask.task.id, claimedTask.attemptN);
10319
+ const selected = runtimeForClaimedTask(runtimes, claimedTask);
10320
+ const resolved = await slotRegistry.findLatestSlotByTaskAttempt(claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN);
10178
10321
  return finalizeTask(ctx.agent, output, {
10179
10322
  task: claimedTask.task,
10180
10323
  slot: resolved ? { expiresAtMs: resolved.slot.expiresAtMs } : null,
10181
10324
  writeCorrelationAnchors: makePrBodyAnchorWriter({
10182
10325
  gh: createGhCliClient(),
10183
- logger: rootLogger
10326
+ logger: rootLogger.child({
10327
+ runtimeProfileId: selected.profile.id,
10328
+ runtimeProfileName: selected.profile.name
10329
+ })
10184
10330
  }),
10185
10331
  log: (msg, err) => rootLogger.warn({ err }, msg)
10186
10332
  });
10187
10333
  },
10188
10334
  executeTask: async (claimedTask, reporter) => {
10335
+ const { common, executionPlans, profile, sandbox, slotIdentity, stateDirs } = runtimeForClaimedTask(runtimes, claimedTask);
10336
+ const taskLogger = rootLogger.child({
10337
+ runtimeProfileId: profile.id,
10338
+ runtimeProfileName: profile.name,
10339
+ provider: profile.provider,
10340
+ model: profile.model
10341
+ });
10189
10342
  let executionPlan;
10190
10343
  try {
10191
10344
  executionPlan = await executionPlans.getOrCreate(claimedTask);
10192
10345
  } catch (err) {
10193
10346
  const message = err instanceof Error ? err.message : String(err);
10194
- rootLogger.warn({
10347
+ taskLogger.warn({
10195
10348
  taskId: claimedTask.task.id,
10196
10349
  attemptN: claimedTask.attemptN,
10197
10350
  err: message
@@ -10215,20 +10368,7 @@ async function runPolling(opts) {
10215
10368
  };
10216
10369
  }
10217
10370
  const sessionDescriptor = executionPlan.descriptor;
10218
- let expired;
10219
- try {
10220
- expired = await slotRegistry.reapExpiredSlots();
10221
- if (expired.length > 0) rootLogger.info({
10222
- expiredCount: expired.length,
10223
- slotKeys: expired.map((item) => item.slot.slotKey)
10224
- }, "agent-daemon.daemon_slots_reaped");
10225
- } catch (err) {
10226
- rootLogger.error({
10227
- phase: "daemon_slot_reap",
10228
- err: err instanceof Error ? err.message : String(err)
10229
- }, "agent-daemon.daemon_slot_reap_failed");
10230
- }
10231
- rootLogger.debug({
10371
+ taskLogger.debug({
10232
10372
  taskId: claimedTask.task.id,
10233
10373
  taskType: claimedTask.task.taskType,
10234
10374
  resumable: sessionDescriptor.policy.resumable,
@@ -10277,6 +10417,10 @@ async function runPolling(opts) {
10277
10417
  };
10278
10418
  if (executionPlan.slotKey && executionPlan.sessionPersistence) await slotRegistry.beginSlot({
10279
10419
  ...slotIdentity,
10420
+ runtimeProfileId: profile.id,
10421
+ provider: profile.provider,
10422
+ model: profile.model,
10423
+ teamId: claimedTask.task.teamId,
10280
10424
  slotKey: executionPlan.slotKey,
10281
10425
  taskType: claimedTask.task.taskType,
10282
10426
  sessionDir: executionPlan.sessionPersistence.sessionDir,
@@ -10286,14 +10430,34 @@ async function runPolling(opts) {
10286
10430
  worktreeBranch: executionPlan.worktreeBranch,
10287
10431
  workspaceKind: executionPlan.workspaceKind,
10288
10432
  lastTaskId: claimedTask.task.id,
10289
- lastAttemptN: claimedTask.attemptN,
10290
- ttlSec: common.warmSessionTtlSec
10433
+ lastAttemptN: claimedTask.attemptN
10434
+ });
10435
+ const rawExecuteTask = createPiTaskExecutor({
10436
+ agentName: common.agent,
10437
+ mountPath: sandbox.rootDir,
10438
+ provider: profile.provider,
10439
+ model: profile.model,
10440
+ sandboxConfig: sandbox.config,
10441
+ makeExecutionPlan: (task) => executionPlans.getOrCreate(task),
10442
+ makeOnTurnEvent: makeTurnEventHandlerFactory(taskLogger),
10443
+ maxTurns: common.maxTurns,
10444
+ maxBashTimeouts: common.maxBashTimeouts
10291
10445
  });
10292
10446
  try {
10293
- return await executeTask(claimedTask, reporter);
10447
+ active = {
10448
+ taskId: claimedTask.task.id,
10449
+ attemptN: claimedTask.attemptN
10450
+ };
10451
+ return await runWithDaemonRuntimeContext({
10452
+ profileId: profile.id,
10453
+ profileName: profile.name,
10454
+ provider: profile.provider,
10455
+ model: profile.model
10456
+ }, () => rawExecuteTask(claimedTask, reporter));
10294
10457
  } finally {
10458
+ active = null;
10295
10459
  executionPlans.delete(claimedTask);
10296
- if (executionPlan.slotKey) await slotRegistry.finishSlot(slotIdentity, executionPlan.slotKey, common.warmSessionTtlSec, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null);
10460
+ if (executionPlan.slotKey) await slotRegistry.finishSlot(claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN, slotIdentity, executionPlan.slotKey, profile.provider, profile.model, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null);
10297
10461
  }
10298
10462
  }
10299
10463
  });
@@ -10312,6 +10476,18 @@ function resolveRecordedWorkspacePath$1(mainRepo, stateRootDir, executionPlan) {
10312
10476
  if (!executionPlan.workspaceId) return null;
10313
10477
  return executionPlan.workspaceMode === "scratch_mount" ? join(stateRootDir, "task-workspaces", executionPlan.workspaceId) : join(mainRepo, ".worktrees", executionPlan.workspaceId);
10314
10478
  }
10479
+ function runtimeForClaimedTask(runtimes, claimedTask) {
10480
+ if (!claimedTask.profileId) throw new Error(`Claimed task ${claimedTask.task.id} did not include a selected runtime profile`);
10481
+ return requireRuntime(runtimes, claimedTask.profileId);
10482
+ }
10483
+ function requireRuntime(runtimes, profileId) {
10484
+ const runtime = runtimes.get(profileId);
10485
+ if (!runtime) throw new Error(`No runtime profile configured for ${profileId}`);
10486
+ return runtime;
10487
+ }
10488
+ function parseProfileValues(raw) {
10489
+ return (raw ?? []).map((s) => s.trim()).filter((s) => s.length > 0);
10490
+ }
10315
10491
  function parseCsv(raw) {
10316
10492
  return (raw ?? "").split(",").map((s) => s.trim()).filter((s) => s.length > 0);
10317
10493
  }
@@ -10347,6 +10523,7 @@ async function runOnce(argv) {
10347
10523
  type: "string",
10348
10524
  short: "t"
10349
10525
  },
10526
+ team: { type: "string" },
10350
10527
  sandbox: { type: "string" },
10351
10528
  profile: { type: "string" }
10352
10529
  }
@@ -10357,9 +10534,14 @@ async function runOnce(argv) {
10357
10534
  return 1;
10358
10535
  }
10359
10536
  const taskId = values["task-id"];
10537
+ if (!values.profile) {
10538
+ console.error("Missing required flag: --profile\n");
10539
+ console.error(ONCE_HELP);
10540
+ return 1;
10541
+ }
10360
10542
  let opts;
10361
10543
  try {
10362
- opts = parseCommonOptions(values, { requireProviderModel: !values.profile });
10544
+ opts = parseCommonOptions(values);
10363
10545
  } catch (err) {
10364
10546
  if (err instanceof MissingRequiredOptionError) {
10365
10547
  console.error(`${err.message}\n`);
@@ -10368,45 +10550,37 @@ async function runOnce(argv) {
10368
10550
  }
10369
10551
  throw err;
10370
10552
  }
10371
- if (values.profile && values.sandbox) {
10372
- console.error("Cannot use --sandbox with --profile. Remote runtime profiles define sandbox policy.");
10553
+ if (values.sandbox) {
10554
+ console.error("Cannot use --sandbox. Remote runtime profiles define sandbox policy.");
10373
10555
  return 1;
10374
10556
  }
10375
10557
  const cfg = loadConfig();
10376
10558
  const ctx = await resolveAgentContext(opts.agent);
10377
- const profile = values.profile ? await resolveRuntimeProfile({
10559
+ const profile = await resolveRuntimeProfile({
10378
10560
  agent: ctx.agent,
10379
10561
  profile: values.profile,
10562
+ teamId: values.team,
10380
10563
  cwd: process.cwd()
10381
- }) : null;
10382
- if (profile) {
10383
- validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
10384
- opts = parseCommonOptions(values, {
10385
- requireProviderModel: false,
10386
- runtimeDefaults: {
10387
- leaseTtlSec: profile.leaseTtlSec,
10388
- heartbeatIntervalMs: profile.heartbeatIntervalMs,
10389
- maxBatchSize: profile.maxBatchSize,
10390
- warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
10391
- }
10392
- });
10393
- }
10394
- const provider = profile?.provider ?? opts.provider;
10395
- const model = profile?.model ?? opts.model;
10396
- if (!provider || !model) throw new Error("provider/model missing after runtime profile resolution");
10397
- const sandbox = profile ? {
10564
+ });
10565
+ validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
10566
+ opts = parseCommonOptions(values, { runtimeDefaults: {
10567
+ leaseTtlSec: profile.leaseTtlSec,
10568
+ heartbeatIntervalMs: profile.heartbeatIntervalMs,
10569
+ maxBatchSize: profile.maxBatchSize,
10570
+ warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
10571
+ } });
10572
+ const sandbox = {
10398
10573
  config: profile.sandboxConfig,
10399
10574
  rootDir: profile.mountPath,
10400
10575
  path: profile.source
10401
- } : resolveSandbox(process.cwd(), values.sandbox);
10576
+ };
10402
10577
  const piAgentDir = ensurePiAgentDir(sandbox.rootDir, cfg.piCodingAgentDir);
10403
10578
  activatePiCodingAgentDir(piAgentDir.path);
10404
10579
  const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
10405
- const slotRegistry = new DaemonSlotRegistry(resolveDaemonStateStorageConfig(stateDirs.registryDbPath, cfg.agentDaemonStateDatabaseUrl));
10580
+ const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
10406
10581
  const slotIdentity = {
10407
10582
  agentName: opts.agent,
10408
- provider,
10409
- model
10583
+ runtimeProfileId: profile.id
10410
10584
  };
10411
10585
  const mainRepo = findMainWorktree();
10412
10586
  const executionPlans = createExecutionPlanCache({
@@ -10422,9 +10596,9 @@ async function runOnce(argv) {
10422
10596
  resourceAttributes: {
10423
10597
  "moltnet.task.id": taskId,
10424
10598
  "moltnet.agent.name": opts.agent,
10425
- "moltnet.llm.provider": provider,
10426
- "moltnet.llm.model": model,
10427
- ...profile ? { "moltnet.daemon_profile.id": profile.id } : {}
10599
+ "moltnet.llm.provider": profile.provider,
10600
+ "moltnet.llm.model": profile.model,
10601
+ "moltnet.runtime_profile.id": profile.id
10428
10602
  }
10429
10603
  });
10430
10604
  const { logger, shutdown: shutdownLogger } = createRootLogger({
@@ -10434,12 +10608,10 @@ async function runOnce(argv) {
10434
10608
  const rootLogger = logger.child({
10435
10609
  mode: "once",
10436
10610
  agent: opts.agent,
10437
- provider,
10438
- model,
10439
- ...profile ? {
10440
- daemonProfileId: profile.id,
10441
- daemonProfileName: profile.name
10442
- } : {}
10611
+ provider: profile.provider,
10612
+ model: profile.model,
10613
+ runtimeProfileId: profile.id,
10614
+ runtimeProfileName: profile.name
10443
10615
  });
10444
10616
  rootLogger.info({
10445
10617
  sandbox: sandbox.path,
@@ -10447,11 +10619,9 @@ async function runOnce(argv) {
10447
10619
  leaseTtlSec: opts.leaseTtlSec,
10448
10620
  heartbeatIntervalMs: opts.heartbeatIntervalMs,
10449
10621
  warmSessionTtlSec: opts.warmSessionTtlSec,
10450
- ...profile ? {
10451
- profileId: profile.id,
10452
- profileSessionTtlSec: profile.sessionTtlSec,
10453
- profileWorkspaceTtlSec: profile.workspaceTtlSec
10454
- } : {},
10622
+ profileId: profile.id,
10623
+ profileSessionTtlSec: profile.sessionTtlSec,
10624
+ profileWorkspaceTtlSec: profile.workspaceTtlSec,
10455
10625
  piAgentDir: piAgentDir.path,
10456
10626
  piAgentDirSource: piAgentDir.source
10457
10627
  }, "agent-daemon.starting");
@@ -10485,8 +10655,8 @@ async function runOnce(argv) {
10485
10655
  const rawExecuteTask = createPiTaskExecutor({
10486
10656
  agentName: opts.agent,
10487
10657
  mountPath: sandbox.rootDir,
10488
- provider,
10489
- model,
10658
+ provider: profile.provider,
10659
+ model: profile.model,
10490
10660
  sandboxConfig: sandbox.config,
10491
10661
  makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
10492
10662
  onTurnEvent: makeTurnEventHandler(rootLogger, { taskId }),
@@ -10494,18 +10664,6 @@ async function runOnce(argv) {
10494
10664
  maxBashTimeouts: opts.maxBashTimeouts
10495
10665
  });
10496
10666
  const executeTask = async (claimedTask, reporter) => {
10497
- try {
10498
- const expired = await slotRegistry.reapExpiredSlots();
10499
- if (expired.length > 0) rootLogger.info({
10500
- expiredCount: expired.length,
10501
- slotKeys: expired.map((item) => item.slot.slotKey)
10502
- }, "agent-daemon.daemon_slots_reaped");
10503
- } catch (err) {
10504
- rootLogger.error({
10505
- phase: "daemon_slot_reap",
10506
- err: err instanceof Error ? err.message : String(err)
10507
- }, "agent-daemon.daemon_slot_reap_failed");
10508
- }
10509
10667
  let executionPlan;
10510
10668
  try {
10511
10669
  executionPlan = await executionPlans.getOrCreate(claimedTask);
@@ -10536,6 +10694,10 @@ async function runOnce(argv) {
10536
10694
  }
10537
10695
  if (executionPlan.slotKey && executionPlan.sessionPersistence) await slotRegistry.beginSlot({
10538
10696
  ...slotIdentity,
10697
+ runtimeProfileId: profile.id,
10698
+ provider: profile.provider,
10699
+ model: profile.model,
10700
+ teamId: claimedTask.task.teamId,
10539
10701
  slotKey: executionPlan.slotKey,
10540
10702
  taskType: claimedTask.task.taskType,
10541
10703
  sessionDir: executionPlan.sessionPersistence.sessionDir,
@@ -10545,16 +10707,20 @@ async function runOnce(argv) {
10545
10707
  worktreeBranch: executionPlan.worktreeBranch,
10546
10708
  workspaceKind: executionPlan.workspaceKind,
10547
10709
  lastTaskId: claimedTask.task.id,
10548
- lastAttemptN: claimedTask.attemptN,
10549
- ttlSec: opts.warmSessionTtlSec
10710
+ lastAttemptN: claimedTask.attemptN
10550
10711
  });
10551
10712
  activeAttemptN = claimedTask.attemptN;
10552
10713
  try {
10553
- return await rawExecuteTask(claimedTask, reporter);
10714
+ return await runWithDaemonRuntimeContext({
10715
+ profileId: profile.id,
10716
+ profileName: profile.name,
10717
+ provider: profile.provider,
10718
+ model: profile.model
10719
+ }, () => rawExecuteTask(claimedTask, reporter));
10554
10720
  } finally {
10555
10721
  activeAttemptN = null;
10556
10722
  executionPlans.delete(claimedTask);
10557
- if (executionPlan.slotKey) await slotRegistry.finishSlot(slotIdentity, executionPlan.slotKey, opts.warmSessionTtlSec, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null);
10723
+ if (executionPlan.slotKey) await slotRegistry.finishSlot(claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN, slotIdentity, executionPlan.slotKey, profile.provider, profile.model, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null);
10558
10724
  }
10559
10725
  };
10560
10726
  const writeCorrelationAnchors = makePrBodyAnchorWriter({
@@ -10567,7 +10733,7 @@ async function runOnce(argv) {
10567
10733
  agent: ctx.agent,
10568
10734
  taskId,
10569
10735
  leaseTtlSec: opts.leaseTtlSec,
10570
- ...profile ? { profileId: profile.id } : {}
10736
+ profileId: profile.id
10571
10737
  }),
10572
10738
  makeReporter: () => new ApiTaskReporter({
10573
10739
  tasks: ctx.agent.tasks,
@@ -10577,7 +10743,7 @@ async function runOnce(argv) {
10577
10743
  flushIntervalMs: opts.flushIntervalMs
10578
10744
  }),
10579
10745
  onTaskFinished: async (output, claimedTask) => {
10580
- const resolved = await slotRegistry.findLatestProducerSlotByTaskAttempt(claimedTask.task.id, claimedTask.attemptN);
10746
+ const resolved = await slotRegistry.findLatestSlotByTaskAttempt(claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN);
10581
10747
  return finalizeTask(ctx.agent, output, {
10582
10748
  task: claimedTask.task,
10583
10749
  slot: resolved ? { expiresAtMs: resolved.slot.expiresAtMs } : null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.19.1",
3
+ "version": "0.21.0",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "description": "MoltNet agent daemon — claims and executes tasks (fulfill_brief, assess_brief) from the MoltNet task-service via Pi-headless. CLI: moltnet-agent.",
@@ -45,10 +45,9 @@
45
45
  "@opentelemetry/semantic-conventions": "^1.39.0",
46
46
  "pino": "^10.3.1",
47
47
  "pino-pretty": "^13.1.3",
48
- "@themoltnet/agent-daemon-state": "0.3.0",
49
- "@themoltnet/sdk": "0.108.0",
50
- "@themoltnet/agent-runtime": "0.26.0",
51
- "@themoltnet/pi-extension": "0.26.0"
48
+ "@themoltnet/agent-runtime": "0.28.0",
49
+ "@themoltnet/pi-extension": "0.26.2",
50
+ "@themoltnet/sdk": "0.109.0"
52
51
  },
53
52
  "devDependencies": {
54
53
  "tsx": "^4.7.0",
@@ -57,8 +56,8 @@
57
56
  "vitest": "^3.0.0",
58
57
  "@moltnet/crypto-service": "0.1.0",
59
58
  "@moltnet/observability": "0.1.0",
60
- "@moltnet/tasks": "0.1.0",
61
- "@moltnet/bootstrap": "0.1.0"
59
+ "@moltnet/bootstrap": "0.1.0",
60
+ "@moltnet/tasks": "0.1.0"
62
61
  },
63
62
  "nx": {
64
63
  "tags": [
@@ -94,8 +93,7 @@
94
93
  "dev": "tsx watch src/main.ts",
95
94
  "start": "node dist/main.js",
96
95
  "build": "vite build",
97
- "check:pack": "tsx ../../tools/src/check-pack.ts --package . && pnpm run smoke:pack && pnpm run smoke:pack:state",
98
- "smoke:pack": "tsx ../../tools/src/smoke-pack.ts --package . --bin moltnet-agent --args --help --expect \"long-running task worker for MoltNet\"",
99
- "smoke:pack:state": "tsx ../../tools/src/smoke-agent-daemon-state-pack.ts"
96
+ "check:pack": "tsx ../../tools/src/check-pack.ts --package . && pnpm run smoke:pack",
97
+ "smoke:pack": "tsx ../../tools/src/smoke-pack.ts --package . --bin moltnet-agent --args --help --expect \"long-running task worker for MoltNet\""
100
98
  }
101
99
  }