@themoltnet/agent-daemon 0.20.0 → 0.22.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 +11 -17
  2. package/dist/main.js +287 -254
  3. package/package.json +6 -6
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
  {
@@ -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
@@ -12,7 +12,7 @@ import { parseArgs, promisify } from "node:util";
12
12
  import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource } from "@themoltnet/agent-runtime";
13
13
  import { createPiTaskExecutor, findMainWorktree } from "@themoltnet/pi-extension";
14
14
  import { execFile, execFileSync } from "node:child_process";
15
- import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
15
+ import { accessSync, constants, existsSync, mkdirSync, readdirSync } from "node:fs";
16
16
  import { MoltNetError, connect } from "@themoltnet/sdk";
17
17
  import { once } from "node:events";
18
18
  import { pino, transport } from "pino";
@@ -21,6 +21,7 @@ import { resourceFromAttributes } from "@opentelemetry/resources";
21
21
  import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
22
22
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
23
23
  import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
24
+ import { AsyncLocalStorage } from "node:async_hooks";
24
25
  //#region ../../libs/observability/src/instrumentation.ts
25
26
  /**
26
27
  * Register OTel auto-instrumentation for common Node.js modules.
@@ -4571,7 +4572,7 @@ _Object_({
4571
4572
  minLength: 1,
4572
4573
  maxLength: 100
4573
4574
  }),
4574
- daemonProfileId: Union([String$1({ format: "uuid" }), Null()]),
4575
+ runtimeProfileId: Union([String$1({ format: "uuid" }), Null()]),
4575
4576
  provider: String$1({
4576
4577
  minLength: 1,
4577
4578
  maxLength: 100
@@ -4602,7 +4603,7 @@ _Object_({
4602
4603
  minLength: 1,
4603
4604
  maxLength: 100
4604
4605
  }),
4605
- daemonProfileId: Optional(String$1({ format: "uuid" })),
4606
+ runtimeProfileId: String$1({ format: "uuid" }),
4606
4607
  provider: String$1({
4607
4608
  minLength: 1,
4608
4609
  maxLength: 100
@@ -4633,6 +4634,7 @@ _Object_({
4633
4634
  minLength: 1,
4634
4635
  maxLength: 100
4635
4636
  }),
4637
+ runtimeProfileId: String$1({ format: "uuid" }),
4636
4638
  provider: String$1({
4637
4639
  minLength: 1,
4638
4640
  maxLength: 100
@@ -9121,23 +9123,16 @@ _Object_({
9121
9123
  //#region src/lib/help.ts
9122
9124
  var COMMON_REQUIRED_FLAGS = `\
9123
9125
  -a, --agent <name> MoltNet agent identity. Reads credentials
9124
- from <repo-root>/.moltnet/<name>/moltnet.json.`;
9125
- var COMMON_MODEL_FLAGS = `\
9126
- -p, --provider <id> LLM provider id (e.g. anthropic, openai-codex).
9127
- -m, --model <id> LLM model id for the provider (e.g.
9128
- claude-sonnet-4-5, gpt-5.3-codex). Required
9129
- unless --profile is set.`;
9126
+ from <agent-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.`;
9130
9131
  var COMMON_OPTIONAL_FLAGS = `\
9131
- --sandbox <path> Path to sandbox.json. Default: search up from
9132
- the daemon's CWD until found. The directory
9133
- containing sandbox.json is also used as the
9134
- VM mountPath. Cannot be used with --profile.
9135
- --profile <uuid|name> Remote runtime profile. When set, provider,
9136
- model, and sandbox policy come from the
9137
- profile; task listing/claiming is restricted
9138
- to unrestricted tasks plus tasks allowing this
9139
- profile. requiredEnv/requiredTools are checked
9140
- before claiming. Name lookup is team-scoped.
9132
+ --sandbox <path> Deprecated. Remote runtime profiles define
9133
+ sandbox policy.
9134
+ --agent-root <path> Directory that owns .moltnet/<agent>. Default:
9135
+ CWD, with git root fallback when available.
9141
9136
  --lease-ttl-sec <n> Sliding liveness window. Silence longer than
9142
9137
  this ends the attempt with lease_expired.
9143
9138
  Default: 300.
@@ -9177,24 +9172,21 @@ Commands:
9177
9172
  Run \`agent-daemon <command> --help\` for command-specific flags.
9178
9173
 
9179
9174
  Prerequisites (all subcommands):
9180
- - <repo-root>/.moltnet/<agent>/moltnet.json — credentials (see --agent)
9181
- - sandbox.json or --profile — local sandbox config is resolved by
9182
- searching up from CWD, or pass --sandbox <path>. With --profile, the
9183
- remote runtime profile supplies provider/model/sandbox policy and CWD
9184
- is used as the VM mountPath.
9175
+ - <agent-root>/.moltnet/<agent>/moltnet.json — credentials (see --agent-root)
9176
+ - --profile — remote runtime profile supplies provider/model/sandbox
9177
+ policy and CWD is used as the VM mountPath.
9185
9178
 
9186
9179
  Registered task types: ${knownTaskTypesList()}`;
9187
9180
  var POLL_HELP = `\
9188
9181
  agent-daemon poll — long-running task worker.
9189
9182
 
9190
9183
  Usage:
9191
- agent-daemon poll --team <uuid> --agent <name> --provider <p> --model <m> [...]
9184
+ agent-daemon poll --team <uuid> --agent <name> --profile <uuid|name> [...]
9192
9185
 
9193
9186
  Required:
9194
9187
  --team <uuid> Team whose queue to serve. The daemon must be
9195
9188
  a member of this team (canAccessTeam permit).
9196
9189
  ${COMMON_REQUIRED_FLAGS}
9197
- ${COMMON_MODEL_FLAGS}
9198
9190
 
9199
9191
  Optional:
9200
9192
  --task-types <csv> Whitelist of task types to claim. Default:
@@ -9211,38 +9203,38 @@ Example:
9211
9203
  --team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
9212
9204
  --task-types curate_pack,fulfill_brief \\
9213
9205
  --agent legreffier \\
9214
- --provider anthropic \\
9215
- --model claude-sonnet-4-5
9206
+ --profile github-linear \\
9207
+ --profile local-fallback
9216
9208
 
9217
9209
  Stops cleanly on SIGINT/SIGTERM (drains the in-flight task before exit).`;
9218
9210
  var ONCE_HELP = `\
9219
9211
  agent-daemon once — execute one specific queued task by id, then exit.
9220
9212
 
9221
9213
  Usage:
9222
- agent-daemon once --task-id <uuid> --agent <name> --provider <p> --model <m> [...]
9214
+ agent-daemon once --task-id <uuid> --agent <name> --profile <uuid|name> [...]
9223
9215
 
9224
9216
  Required:
9225
9217
  -t, --task-id <uuid> Task to claim and execute. Must already be
9226
9218
  in 'queued' status.
9227
9219
  ${COMMON_REQUIRED_FLAGS}
9228
- ${COMMON_MODEL_FLAGS}
9229
9220
 
9230
9221
  Optional:
9222
+ --team <uuid> Team scope for resolving --profile by name.
9223
+ Required only when --profile is a name.
9231
9224
  ${COMMON_OPTIONAL_FLAGS}
9232
9225
 
9233
9226
  Example:
9234
9227
  agent-daemon once \\
9235
9228
  --task-id 26004a77-bc10-43ef-a79f-c8e62faf59b1 \\
9236
9229
  --agent legreffier \\
9237
- --provider anthropic \\
9238
- --model claude-sonnet-4-5
9230
+ --profile github-linear
9239
9231
 
9240
9232
  Exits 0 on completed, 1 on failed/cancelled/runtime-error.`;
9241
9233
  var DRAIN_HELP = `\
9242
9234
  agent-daemon drain — poll until the queue is empty, then exit.
9243
9235
 
9244
9236
  Usage:
9245
- agent-daemon drain --team <uuid> --agent <name> --provider <p> --model <m> [...]
9237
+ agent-daemon drain --team <uuid> --agent <name> --profile <uuid|name> [...]
9246
9238
 
9247
9239
  Same flags as \`poll\`. The only behavioural difference: \`drain\` exits
9248
9240
  when a list call confirms no claimable tasks remain (vs \`poll\` which
@@ -9251,7 +9243,6 @@ sleeps and retries forever).
9251
9243
  Required:
9252
9244
  --team <uuid> Team whose queue to drain.
9253
9245
  ${COMMON_REQUIRED_FLAGS}
9254
- ${COMMON_MODEL_FLAGS}
9255
9246
 
9256
9247
  Optional:
9257
9248
  --task-types <csv> Whitelist. Known types: ${knownTaskTypesList()}
@@ -9266,8 +9257,7 @@ Example:
9266
9257
  --team 6743b4b1-6b93-46e2-a048-19490f04f91a \\
9267
9258
  --task-types judge_pack \\
9268
9259
  --agent legreffier \\
9269
- --provider anthropic \\
9270
- --model claude-sonnet-4-5`;
9260
+ --profile eval-judge`;
9271
9261
  function isHelpFlag(args) {
9272
9262
  return args.includes("--help") || args.includes("-h");
9273
9263
  }
@@ -9290,18 +9280,34 @@ function activatePiCodingAgentDir(path) {
9290
9280
  /**
9291
9281
  * Resolve the agent's MoltNet credentials directory and connect via SDK.
9292
9282
  *
9293
- * Looks under `<repo-root>/.moltnet/<agentName>/`. Fails fast if the dir
9294
- * is missing — credentials are required, the daemon never falls back to
9295
- * unauthenticated calls.
9283
+ * Looks under an explicit agent root first, then falls back to the git root
9284
+ * when available. Fails fast if the dir is missing — credentials are required,
9285
+ * the daemon never falls back to unauthenticated calls.
9296
9286
  */
9297
- async function resolveAgentContext(agentName) {
9287
+ async function resolveAgentContext(agentName, options = {}) {
9298
9288
  if (!/^[a-zA-Z0-9_-]+$/.test(agentName)) throw new Error(`Invalid agent name "${agentName}": must match /^[a-zA-Z0-9_-]+$/`);
9299
- const agentDir = join(execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim(), ".moltnet", agentName);
9300
- if (!existsSync(join(agentDir, "moltnet.json"))) throw new Error(`Missing credentials at ${agentDir}/moltnet.json. Run the agent onboarding flow first.`);
9301
- return {
9302
- agentDir,
9303
- agent: await connect({ configDir: agentDir })
9304
- };
9289
+ const roots = resolveCredentialRoots(options.agentRootDir);
9290
+ for (const rootDir of roots) {
9291
+ const agentDir = join(rootDir, ".moltnet", agentName);
9292
+ if (existsSync(join(agentDir, "moltnet.json"))) return {
9293
+ agentDir,
9294
+ agentRootDir: rootDir,
9295
+ agent: await connect({ configDir: agentDir })
9296
+ };
9297
+ }
9298
+ const tried = roots.map((root) => join(root, ".moltnet", agentName));
9299
+ throw new Error(`Missing credentials for ${agentName}. Checked ${tried.join(", ")}. Run the agent onboarding flow first.`);
9300
+ }
9301
+ function resolveCredentialRoots(agentRootDir) {
9302
+ const roots = agentRootDir ? [agentRootDir] : [];
9303
+ try {
9304
+ const gitRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
9305
+ encoding: "utf8",
9306
+ stdio: "pipe"
9307
+ }).trim();
9308
+ if (!roots.includes(gitRoot)) roots.push(gitRoot);
9309
+ } catch {}
9310
+ return roots;
9305
9311
  }
9306
9312
  //#endregion
9307
9313
  //#region src/lib/correlation.ts
@@ -9472,10 +9478,8 @@ function buildDaemonSlotId(identity, slotKey) {
9472
9478
  return [
9473
9479
  "agent",
9474
9480
  slugSlotIdentityComponent(identity.agentName),
9475
- "provider",
9476
- slugSlotIdentityComponent(identity.provider),
9477
- "model",
9478
- slugSlotIdentityComponent(identity.model),
9481
+ "profile",
9482
+ slugSlotIdentityComponent(identity.runtimeProfileId),
9479
9483
  "key",
9480
9484
  slotKey
9481
9485
  ].join(":");
@@ -9790,7 +9794,6 @@ var MissingRequiredOptionError = class extends Error {
9790
9794
  }
9791
9795
  };
9792
9796
  function parseCommonOptions(args, options = {}) {
9793
- const requireProviderModel = options.requireProviderModel ?? true;
9794
9797
  const runtimeDefaults = {
9795
9798
  leaseTtlSec: options.runtimeDefaults?.leaseTtlSec ?? DEFAULTS.leaseTtlSec,
9796
9799
  heartbeatIntervalMs: options.runtimeDefaults?.heartbeatIntervalMs ?? DEFAULTS.heartbeatIntervalMs,
@@ -9798,13 +9801,9 @@ function parseCommonOptions(args, options = {}) {
9798
9801
  warmSessionTtlSec: options.runtimeDefaults?.warmSessionTtlSec ?? DEFAULTS.warmSessionTtlSec
9799
9802
  };
9800
9803
  if (!args.agent) throw new MissingRequiredOptionError("agent");
9801
- if (requireProviderModel && !args.provider) throw new MissingRequiredOptionError("provider");
9802
- if (requireProviderModel && !args.model) throw new MissingRequiredOptionError("model");
9803
9804
  if (!/^[a-zA-Z0-9_-]+$/.test(args.agent)) throw new Error(`Invalid --agent "${args.agent}": must match /^[a-zA-Z0-9_-]+$/`);
9804
9805
  return {
9805
9806
  agent: args.agent,
9806
- ...args.provider ? { provider: args.provider } : {},
9807
- ...args.model ? { model: args.model } : {},
9808
9807
  leaseTtlSec: parsePositiveInt(args["lease-ttl-sec"], "lease-ttl-sec", runtimeDefaults.leaseTtlSec),
9809
9808
  heartbeatIntervalMs: parseNonNegativeInt(args["heartbeat-interval-ms"], "heartbeat-interval-ms", runtimeDefaults.heartbeatIntervalMs),
9810
9809
  maxBatchSize: parsePositiveInt(args["max-batch-size"], "max-batch-size", runtimeDefaults.maxBatchSize),
@@ -9833,14 +9832,7 @@ function commonOptionDefs() {
9833
9832
  type: "string",
9834
9833
  short: "a"
9835
9834
  },
9836
- model: {
9837
- type: "string",
9838
- short: "m"
9839
- },
9840
- provider: {
9841
- type: "string",
9842
- short: "p"
9843
- },
9835
+ "agent-root": { type: "string" },
9844
9836
  "lease-ttl-sec": { type: "string" },
9845
9837
  "heartbeat-interval-ms": { type: "string" },
9846
9838
  "max-batch-size": { type: "string" },
@@ -9919,6 +9911,12 @@ function ensurePiAgentDir(repoRoot, explicitPath) {
9919
9911
  };
9920
9912
  }
9921
9913
  //#endregion
9914
+ //#region src/lib/runtime-context.ts
9915
+ var storage = new AsyncLocalStorage();
9916
+ function runWithDaemonRuntimeContext(context, callback) {
9917
+ return storage.run(context, callback);
9918
+ }
9919
+ //#endregion
9922
9920
  //#region src/lib/runtime-profile.ts
9923
9921
  var RuntimeProfilePrerequisiteError = class extends Error {
9924
9922
  constructor(profileName, missingEnv, missingTools) {
@@ -9952,6 +9950,22 @@ async function resolveRuntimeProfile(options) {
9952
9950
  source: `runtime-profile:${profile.id}`
9953
9951
  };
9954
9952
  }
9953
+ async function resolveRuntimeProfiles(options) {
9954
+ const seen = /* @__PURE__ */ new Set();
9955
+ const out = [];
9956
+ for (const profile of options.profiles) {
9957
+ const resolved = await resolveRuntimeProfile({
9958
+ agent: options.agent,
9959
+ profile,
9960
+ teamId: options.teamId,
9961
+ cwd: options.cwd
9962
+ });
9963
+ if (seen.has(resolved.id)) continue;
9964
+ seen.add(resolved.id);
9965
+ out.push(resolved);
9966
+ }
9967
+ return out;
9968
+ }
9955
9969
  function validateRuntimeProfilePrerequisites(profile, env, pathValue) {
9956
9970
  const missingEnv = profile.requiredEnv.filter((name) => !env[name]);
9957
9971
  const missingTools = profile.requiredTools.filter((tool) => !isExecutableOnPath(tool, pathValue));
@@ -9986,12 +10000,12 @@ function isExecutable(path) {
9986
10000
  //#endregion
9987
10001
  //#region src/lib/runtime-slots.ts
9988
10002
  function createApiRuntimeSlotStore(args) {
9989
- const { agent, daemonProfileId } = args;
10003
+ const { agent } = args;
9990
10004
  return {
9991
10005
  async beginSlot(input) {
9992
10006
  await agent.runtimeSlots.begin({
9993
10007
  agentName: input.agentName,
9994
- daemonProfileId: daemonProfileId ?? void 0,
10008
+ runtimeProfileId: input.runtimeProfileId,
9995
10009
  lastAttemptN: input.lastAttemptN,
9996
10010
  lastTaskId: input.lastTaskId,
9997
10011
  model: input.model,
@@ -10006,12 +10020,13 @@ function createApiRuntimeSlotStore(args) {
10006
10020
  worktreePath: input.worktreePath ?? void 0
10007
10021
  }, { teamId: input.teamId });
10008
10022
  },
10009
- async finishSlot(teamId, taskId, attemptN, identity, slotKey, sessionPath) {
10023
+ async finishSlot(teamId, taskId, attemptN, identity, slotKey, provider, model, sessionPath) {
10010
10024
  await agent.runtimeSlots.finish({
10011
10025
  agentName: identity.agentName,
10012
10026
  attemptN,
10013
- model: identity.model,
10014
- provider: identity.provider,
10027
+ runtimeProfileId: identity.runtimeProfileId,
10028
+ model,
10029
+ provider,
10015
10030
  sessionPath: sessionPath ?? void 0,
10016
10031
  slotKey,
10017
10032
  taskId
@@ -10041,34 +10056,6 @@ function createApiRuntimeSlotStore(args) {
10041
10056
  };
10042
10057
  }
10043
10058
  //#endregion
10044
- //#region src/lib/sandbox.ts
10045
- function resolveSandbox(startDir, explicitPath) {
10046
- const path = explicitPath ? isAbsolute(explicitPath) ? explicitPath : resolve(startDir, explicitPath) : findUp(startDir, "sandbox.json");
10047
- 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.`);
10048
- let config;
10049
- try {
10050
- config = JSON.parse(readFileSync(path, "utf8"));
10051
- } catch (err) {
10052
- const isEnoent = err instanceof Error && "code" in err && err.code === "ENOENT";
10053
- throw new Error(isEnoent ? `sandbox.json not found at ${path}.` : `Failed to read sandbox.json at ${path}: ${err instanceof Error ? err.message : String(err)}`);
10054
- }
10055
- return {
10056
- config,
10057
- rootDir: dirname(path),
10058
- path
10059
- };
10060
- }
10061
- function findUp(startDir, filename) {
10062
- let dir = resolve(startDir);
10063
- while (true) {
10064
- const candidate = resolve(dir, filename);
10065
- if (existsSync(candidate)) return candidate;
10066
- const parent = dirname(dir);
10067
- if (parent === dir) return null;
10068
- dir = parent;
10069
- }
10070
- }
10071
- //#endregion
10072
10059
  //#region src/lib/shutdown-signal.ts
10073
10060
  function installShutdownSignalHandlers(opts) {
10074
10061
  const proc = opts.proc ?? process;
@@ -10150,7 +10137,10 @@ async function runPolling(opts) {
10150
10137
  "max-poll-interval-ms": { type: "string" },
10151
10138
  "list-limit": { type: "string" },
10152
10139
  sandbox: { type: "string" },
10153
- profile: { type: "string" }
10140
+ profile: {
10141
+ type: "string",
10142
+ multiple: true
10143
+ }
10154
10144
  }
10155
10145
  });
10156
10146
  if (!values.team) {
@@ -10159,6 +10149,12 @@ async function runPolling(opts) {
10159
10149
  return 1;
10160
10150
  }
10161
10151
  const teamId = values.team;
10152
+ const profileValues = parseProfileValues(values.profile);
10153
+ if (profileValues.length === 0) {
10154
+ console.error("Missing required flag: --profile\n");
10155
+ console.error(opts.helpText);
10156
+ return 1;
10157
+ }
10162
10158
  let taskTypes;
10163
10159
  try {
10164
10160
  taskTypes = validateTaskTypes(parseCsv(values["task-types"]));
@@ -10168,9 +10164,9 @@ async function runPolling(opts) {
10168
10164
  return 1;
10169
10165
  }
10170
10166
  const diaryIds = parseCsv(values["diary-ids"]);
10171
- let common;
10167
+ let baseCommon;
10172
10168
  try {
10173
- common = parseCommonOptions(values, { requireProviderModel: !values.profile });
10169
+ baseCommon = parseCommonOptions(values);
10174
10170
  } catch (err) {
10175
10171
  if (err instanceof MissingRequiredOptionError) {
10176
10172
  console.error(`${err.message}\n`);
@@ -10182,84 +10178,82 @@ async function runPolling(opts) {
10182
10178
  const pollIntervalMs = optionalPositiveInt(values["poll-interval-ms"], "poll-interval-ms", 2e3);
10183
10179
  const maxPollIntervalMs = optionalPositiveInt(values["max-poll-interval-ms"], "max-poll-interval-ms", 3e4);
10184
10180
  const listLimit = optionalPositiveInt(values["list-limit"], "list-limit", 10);
10185
- if (values.profile && values.sandbox) {
10186
- console.error(`[${opts.modeLabel}] Cannot use --sandbox with --profile. Remote runtime profiles define sandbox policy.`);
10181
+ if (values.sandbox) {
10182
+ console.error(`[${opts.modeLabel}] Cannot use --sandbox. Remote runtime profiles define sandbox policy.`);
10187
10183
  return 1;
10188
10184
  }
10189
10185
  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).`);
10190
10186
  const cfg = loadConfig();
10191
- const ctx = await resolveAgentContext(common.agent);
10192
- const profile = values.profile ? await resolveRuntimeProfile({
10187
+ const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
10188
+ const ctx = await resolveAgentContext(baseCommon.agent, { agentRootDir });
10189
+ const profiles = await resolveRuntimeProfiles({
10193
10190
  agent: ctx.agent,
10194
- profile: values.profile,
10191
+ profiles: profileValues,
10195
10192
  teamId,
10196
10193
  cwd: process.cwd()
10197
- }) : null;
10198
- if (profile) {
10199
- validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
10200
- common = parseCommonOptions(values, {
10201
- requireProviderModel: false,
10202
- runtimeDefaults: {
10203
- leaseTtlSec: profile.leaseTtlSec,
10204
- heartbeatIntervalMs: profile.heartbeatIntervalMs,
10205
- maxBatchSize: profile.maxBatchSize,
10206
- warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
10207
- }
10194
+ });
10195
+ for (const profile of profiles) validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
10196
+ const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
10197
+ const runtimes = /* @__PURE__ */ new Map();
10198
+ for (const profile of profiles) {
10199
+ const common = parseCommonOptions(values, { runtimeDefaults: {
10200
+ leaseTtlSec: profile.leaseTtlSec,
10201
+ heartbeatIntervalMs: profile.heartbeatIntervalMs,
10202
+ maxBatchSize: profile.maxBatchSize,
10203
+ warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
10204
+ } });
10205
+ const sandbox = {
10206
+ config: profile.sandboxConfig,
10207
+ rootDir: profile.mountPath,
10208
+ path: profile.source
10209
+ };
10210
+ const piAgentDir = ensurePiAgentDir(sandbox.rootDir, cfg.piCodingAgentDir);
10211
+ const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
10212
+ const slotIdentity = {
10213
+ agentName: common.agent,
10214
+ runtimeProfileId: profile.id
10215
+ };
10216
+ const executionPlans = createExecutionPlanCache({
10217
+ stateDirs,
10218
+ slotIdentity,
10219
+ warmSessionTtlSec: common.warmSessionTtlSec,
10220
+ slotRegistry
10221
+ });
10222
+ runtimes.set(profile.id, {
10223
+ common,
10224
+ profile,
10225
+ sandbox,
10226
+ stateDirs,
10227
+ piAgentDir,
10228
+ slotIdentity,
10229
+ executionPlans
10208
10230
  });
10209
10231
  }
10210
- const provider = profile?.provider ?? common.provider;
10211
- const model = profile?.model ?? common.model;
10212
- if (!provider || !model) throw new Error("provider/model missing after runtime profile resolution");
10213
- const sandbox = profile ? {
10214
- config: profile.sandboxConfig,
10215
- rootDir: profile.mountPath,
10216
- path: profile.source
10217
- } : resolveSandbox(process.cwd(), values.sandbox);
10218
- const piAgentDir = ensurePiAgentDir(sandbox.rootDir, cfg.piCodingAgentDir);
10232
+ const firstRuntime = runtimes.get(profiles[0].id);
10233
+ if (!firstRuntime) throw new Error("No runtime profiles resolved");
10234
+ const piAgentDir = firstRuntime.piAgentDir;
10219
10235
  activatePiCodingAgentDir(piAgentDir.path);
10220
- const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
10221
- const slotRegistry = createApiRuntimeSlotStore({
10222
- agent: ctx.agent,
10223
- daemonProfileId: profile?.id ?? null
10224
- });
10225
- const slotIdentity = {
10226
- agentName: common.agent,
10227
- provider,
10228
- model
10229
- };
10230
- const mainRepo = findMainWorktree();
10231
- const executionPlans = createExecutionPlanCache({
10232
- stateDirs,
10233
- slotIdentity,
10234
- warmSessionTtlSec: common.warmSessionTtlSec,
10235
- slotRegistry
10236
- });
10237
10236
  const otelShutdown = await initWorkerOtel({
10238
10237
  serviceName: opts.serviceName,
10239
10238
  agentDir: ctx.agentDir,
10240
10239
  endpoint: cfg.otelEndpoint,
10241
10240
  resourceAttributes: {
10242
10241
  "moltnet.team.id": teamId,
10243
- "moltnet.agent.name": common.agent,
10244
- "moltnet.llm.provider": provider,
10245
- "moltnet.llm.model": model,
10246
- ...profile ? { "moltnet.daemon_profile.id": profile.id } : {}
10242
+ "moltnet.agent.name": baseCommon.agent,
10243
+ "moltnet.runtime_profile.count": String(profiles.length),
10244
+ "moltnet.runtime_profile.ids": profiles.map((p) => p.id).join(",")
10247
10245
  }
10248
10246
  });
10249
10247
  const { logger, shutdown: shutdownLogger } = createRootLogger({
10250
10248
  name: `agent-daemon.${opts.modeLabel}`,
10251
- level: cfg.logLevel || (common.debug ? "debug" : "info")
10249
+ level: cfg.logLevel || (baseCommon.debug ? "debug" : "info")
10252
10250
  });
10253
10251
  const rootLogger = logger.child({
10254
10252
  mode: opts.modeLabel,
10255
- agent: common.agent,
10253
+ agent: baseCommon.agent,
10256
10254
  teamId,
10257
- provider,
10258
- model,
10259
- ...profile ? {
10260
- daemonProfileId: profile.id,
10261
- daemonProfileName: profile.name
10262
- } : {}
10255
+ runtimeProfileIds: profiles.map((p) => p.id),
10256
+ runtimeProfileNames: profiles.map((p) => p.name)
10263
10257
  });
10264
10258
  const abort = new AbortController();
10265
10259
  let runtime = null;
@@ -10285,90 +10279,91 @@ async function runPolling(opts) {
10285
10279
  }
10286
10280
  });
10287
10281
  rootLogger.info({
10288
- sandbox: sandbox.path,
10289
10282
  taskTypes: taskTypes.length > 0 ? taskTypes : ["*"],
10290
10283
  diaryIds: diaryIds.length > 0 ? diaryIds : ["*"],
10291
- leaseTtlSec: common.leaseTtlSec,
10292
- heartbeatIntervalMs: common.heartbeatIntervalMs,
10293
- warmSessionTtlSec: common.warmSessionTtlSec,
10294
10284
  pollIntervalMs,
10295
10285
  maxPollIntervalMs,
10296
- ...profile ? {
10297
- profileId: profile.id,
10298
- profileSessionTtlSec: profile.sessionTtlSec,
10299
- profileWorkspaceTtlSec: profile.workspaceTtlSec
10300
- } : {},
10286
+ profiles: profiles.map((profile) => {
10287
+ const runtime = requireRuntime(runtimes, profile.id);
10288
+ return {
10289
+ id: profile.id,
10290
+ name: profile.name,
10291
+ provider: profile.provider,
10292
+ model: profile.model,
10293
+ sandbox: runtime.sandbox.path,
10294
+ leaseTtlSec: runtime.common.leaseTtlSec,
10295
+ heartbeatIntervalMs: runtime.common.heartbeatIntervalMs,
10296
+ warmSessionTtlSec: runtime.common.warmSessionTtlSec,
10297
+ profileSessionTtlSec: profile.sessionTtlSec,
10298
+ profileWorkspaceTtlSec: profile.workspaceTtlSec
10299
+ };
10300
+ }),
10301
10301
  piAgentDir: piAgentDir.path,
10302
10302
  piAgentDirSource: piAgentDir.source
10303
10303
  }, "agent-daemon.starting");
10304
10304
  const outputs = [];
10305
10305
  try {
10306
- const rawExecuteTask = createPiTaskExecutor({
10307
- agentName: common.agent,
10308
- mountPath: sandbox.rootDir,
10309
- provider,
10310
- model,
10311
- sandboxConfig: sandbox.config,
10312
- makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
10313
- makeOnTurnEvent: makeTurnEventHandlerFactory(rootLogger),
10314
- maxTurns: common.maxTurns,
10315
- maxBashTimeouts: common.maxBashTimeouts
10316
- });
10317
- const executeTask = async (claimedTask, reporter) => {
10318
- active = {
10319
- taskId: claimedTask.task.id,
10320
- attemptN: claimedTask.attemptN
10321
- };
10322
- try {
10323
- return await rawExecuteTask(claimedTask, reporter);
10324
- } finally {
10325
- active = null;
10326
- }
10327
- };
10328
10306
  runtime = new AgentRuntime({
10329
10307
  logger: rootLogger,
10330
10308
  source: new PollingApiTaskSource({
10331
10309
  agent: ctx.agent,
10332
10310
  teamId,
10333
10311
  taskTypes: taskTypes.length > 0 ? taskTypes : void 0,
10334
- ...profile ? { profileId: profile.id } : {},
10312
+ profiles: profiles.map((profile) => ({
10313
+ profileId: profile.id,
10314
+ leaseTtlSec: requireRuntime(runtimes, profile.id).common.leaseTtlSec
10315
+ })),
10335
10316
  diaryIds: diaryIds.length > 0 ? diaryIds : void 0,
10336
- leaseTtlSec: common.leaseTtlSec,
10317
+ leaseTtlSec: firstRuntime.common.leaseTtlSec,
10337
10318
  listLimit,
10338
10319
  pollIntervalMs,
10339
10320
  maxPollIntervalMs,
10340
10321
  signal: abort.signal,
10341
10322
  stopWhenEmpty: opts.stopWhenEmpty,
10342
- debug: common.debug,
10323
+ debug: baseCommon.debug,
10343
10324
  logger: rootLogger,
10344
10325
  slotRegistry
10345
10326
  }),
10346
- makeReporter: () => new ApiTaskReporter({
10347
- tasks: ctx.agent.tasks,
10348
- leaseTtlSec: common.leaseTtlSec,
10349
- heartbeatIntervalMs: common.heartbeatIntervalMs,
10350
- maxBatchSize: common.maxBatchSize,
10351
- flushIntervalMs: common.flushIntervalMs
10352
- }),
10327
+ makeReporter: (claimedTask) => {
10328
+ const selected = runtimeForClaimedTask(runtimes, claimedTask);
10329
+ return new ApiTaskReporter({
10330
+ tasks: ctx.agent.tasks,
10331
+ leaseTtlSec: selected.common.leaseTtlSec,
10332
+ heartbeatIntervalMs: selected.common.heartbeatIntervalMs,
10333
+ maxBatchSize: selected.common.maxBatchSize,
10334
+ flushIntervalMs: selected.common.flushIntervalMs
10335
+ });
10336
+ },
10353
10337
  onTaskFinished: async (output, claimedTask) => {
10338
+ const selected = runtimeForClaimedTask(runtimes, claimedTask);
10354
10339
  const resolved = await slotRegistry.findLatestSlotByTaskAttempt(claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN);
10355
10340
  return finalizeTask(ctx.agent, output, {
10356
10341
  task: claimedTask.task,
10357
10342
  slot: resolved ? { expiresAtMs: resolved.slot.expiresAtMs } : null,
10358
10343
  writeCorrelationAnchors: makePrBodyAnchorWriter({
10359
10344
  gh: createGhCliClient(),
10360
- logger: rootLogger
10345
+ logger: rootLogger.child({
10346
+ runtimeProfileId: selected.profile.id,
10347
+ runtimeProfileName: selected.profile.name
10348
+ })
10361
10349
  }),
10362
10350
  log: (msg, err) => rootLogger.warn({ err }, msg)
10363
10351
  });
10364
10352
  },
10365
10353
  executeTask: async (claimedTask, reporter) => {
10354
+ const { common, executionPlans, profile, sandbox, slotIdentity, stateDirs } = runtimeForClaimedTask(runtimes, claimedTask);
10355
+ const taskLogger = rootLogger.child({
10356
+ runtimeProfileId: profile.id,
10357
+ runtimeProfileName: profile.name,
10358
+ provider: profile.provider,
10359
+ model: profile.model
10360
+ });
10366
10361
  let executionPlan;
10367
10362
  try {
10368
10363
  executionPlan = await executionPlans.getOrCreate(claimedTask);
10369
10364
  } catch (err) {
10370
10365
  const message = err instanceof Error ? err.message : String(err);
10371
- rootLogger.warn({
10366
+ taskLogger.warn({
10372
10367
  taskId: claimedTask.task.id,
10373
10368
  attemptN: claimedTask.attemptN,
10374
10369
  err: message
@@ -10392,7 +10387,7 @@ async function runPolling(opts) {
10392
10387
  };
10393
10388
  }
10394
10389
  const sessionDescriptor = executionPlan.descriptor;
10395
- rootLogger.debug({
10390
+ taskLogger.debug({
10396
10391
  taskId: claimedTask.task.id,
10397
10392
  taskType: claimedTask.task.taskType,
10398
10393
  resumable: sessionDescriptor.policy.resumable,
@@ -10441,23 +10436,48 @@ async function runPolling(opts) {
10441
10436
  };
10442
10437
  if (executionPlan.slotKey && executionPlan.sessionPersistence) await slotRegistry.beginSlot({
10443
10438
  ...slotIdentity,
10439
+ runtimeProfileId: profile.id,
10440
+ provider: profile.provider,
10441
+ model: profile.model,
10444
10442
  teamId: claimedTask.task.teamId,
10445
10443
  slotKey: executionPlan.slotKey,
10446
10444
  taskType: claimedTask.task.taskType,
10447
10445
  sessionDir: executionPlan.sessionPersistence.sessionDir,
10448
10446
  sessionPath: resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir),
10449
10447
  workspaceId: executionPlan.workspaceId,
10450
- worktreePath: resolveRecordedWorkspacePath$1(mainRepo, stateDirs.rootDir, executionPlan),
10448
+ worktreePath: resolveRecordedWorkspacePath$1(stateDirs.rootDir, executionPlan),
10451
10449
  worktreeBranch: executionPlan.worktreeBranch,
10452
10450
  workspaceKind: executionPlan.workspaceKind,
10453
10451
  lastTaskId: claimedTask.task.id,
10454
10452
  lastAttemptN: claimedTask.attemptN
10455
10453
  });
10454
+ const rawExecuteTask = createPiTaskExecutor({
10455
+ agentName: common.agent,
10456
+ agentRootDir: ctx.agentRootDir,
10457
+ mountPath: sandbox.rootDir,
10458
+ provider: profile.provider,
10459
+ model: profile.model,
10460
+ sandboxConfig: sandbox.config,
10461
+ makeExecutionPlan: (task) => executionPlans.getOrCreate(task),
10462
+ makeOnTurnEvent: makeTurnEventHandlerFactory(taskLogger),
10463
+ maxTurns: common.maxTurns,
10464
+ maxBashTimeouts: common.maxBashTimeouts
10465
+ });
10456
10466
  try {
10457
- return await executeTask(claimedTask, reporter);
10467
+ active = {
10468
+ taskId: claimedTask.task.id,
10469
+ attemptN: claimedTask.attemptN
10470
+ };
10471
+ return await runWithDaemonRuntimeContext({
10472
+ profileId: profile.id,
10473
+ profileName: profile.name,
10474
+ provider: profile.provider,
10475
+ model: profile.model
10476
+ }, () => rawExecuteTask(claimedTask, reporter));
10458
10477
  } finally {
10478
+ active = null;
10459
10479
  executionPlans.delete(claimedTask);
10460
- if (executionPlan.slotKey) await slotRegistry.finishSlot(claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN, slotIdentity, executionPlan.slotKey, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null);
10480
+ 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);
10461
10481
  }
10462
10482
  }
10463
10483
  });
@@ -10472,9 +10492,21 @@ async function runPolling(opts) {
10472
10492
  await shutdownLogger();
10473
10493
  }
10474
10494
  }
10475
- function resolveRecordedWorkspacePath$1(mainRepo, stateRootDir, executionPlan) {
10495
+ function resolveRecordedWorkspacePath$1(stateRootDir, executionPlan) {
10476
10496
  if (!executionPlan.workspaceId) return null;
10477
- return executionPlan.workspaceMode === "scratch_mount" ? join(stateRootDir, "task-workspaces", executionPlan.workspaceId) : join(mainRepo, ".worktrees", executionPlan.workspaceId);
10497
+ return executionPlan.workspaceMode === "scratch_mount" ? join(stateRootDir, "task-workspaces", executionPlan.workspaceId) : join(findMainWorktree(), ".worktrees", executionPlan.workspaceId);
10498
+ }
10499
+ function runtimeForClaimedTask(runtimes, claimedTask) {
10500
+ if (!claimedTask.profileId) throw new Error(`Claimed task ${claimedTask.task.id} did not include a selected runtime profile`);
10501
+ return requireRuntime(runtimes, claimedTask.profileId);
10502
+ }
10503
+ function requireRuntime(runtimes, profileId) {
10504
+ const runtime = runtimes.get(profileId);
10505
+ if (!runtime) throw new Error(`No runtime profile configured for ${profileId}`);
10506
+ return runtime;
10507
+ }
10508
+ function parseProfileValues(raw) {
10509
+ return (raw ?? []).map((s) => s.trim()).filter((s) => s.length > 0);
10478
10510
  }
10479
10511
  function parseCsv(raw) {
10480
10512
  return (raw ?? "").split(",").map((s) => s.trim()).filter((s) => s.length > 0);
@@ -10511,6 +10543,7 @@ async function runOnce(argv) {
10511
10543
  type: "string",
10512
10544
  short: "t"
10513
10545
  },
10546
+ team: { type: "string" },
10514
10547
  sandbox: { type: "string" },
10515
10548
  profile: { type: "string" }
10516
10549
  }
@@ -10521,9 +10554,14 @@ async function runOnce(argv) {
10521
10554
  return 1;
10522
10555
  }
10523
10556
  const taskId = values["task-id"];
10557
+ if (!values.profile) {
10558
+ console.error("Missing required flag: --profile\n");
10559
+ console.error(ONCE_HELP);
10560
+ return 1;
10561
+ }
10524
10562
  let opts;
10525
10563
  try {
10526
- opts = parseCommonOptions(values, { requireProviderModel: !values.profile });
10564
+ opts = parseCommonOptions(values);
10527
10565
  } catch (err) {
10528
10566
  if (err instanceof MissingRequiredOptionError) {
10529
10567
  console.error(`${err.message}\n`);
@@ -10532,50 +10570,40 @@ async function runOnce(argv) {
10532
10570
  }
10533
10571
  throw err;
10534
10572
  }
10535
- if (values.profile && values.sandbox) {
10536
- console.error("Cannot use --sandbox with --profile. Remote runtime profiles define sandbox policy.");
10573
+ if (values.sandbox) {
10574
+ console.error("Cannot use --sandbox. Remote runtime profiles define sandbox policy.");
10537
10575
  return 1;
10538
10576
  }
10539
10577
  const cfg = loadConfig();
10540
- const ctx = await resolveAgentContext(opts.agent);
10541
- const profile = values.profile ? await resolveRuntimeProfile({
10578
+ const initialOpts = opts;
10579
+ const agentRootDir = resolve(process.cwd(), values["agent-root"] ?? process.cwd());
10580
+ const ctx = await resolveAgentContext(initialOpts.agent, { agentRootDir });
10581
+ const profile = await resolveRuntimeProfile({
10542
10582
  agent: ctx.agent,
10543
10583
  profile: values.profile,
10584
+ teamId: values.team,
10544
10585
  cwd: process.cwd()
10545
- }) : null;
10546
- if (profile) {
10547
- validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
10548
- opts = parseCommonOptions(values, {
10549
- requireProviderModel: false,
10550
- runtimeDefaults: {
10551
- leaseTtlSec: profile.leaseTtlSec,
10552
- heartbeatIntervalMs: profile.heartbeatIntervalMs,
10553
- maxBatchSize: profile.maxBatchSize,
10554
- warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
10555
- }
10556
- });
10557
- }
10558
- const provider = profile?.provider ?? opts.provider;
10559
- const model = profile?.model ?? opts.model;
10560
- if (!provider || !model) throw new Error("provider/model missing after runtime profile resolution");
10561
- const sandbox = profile ? {
10586
+ });
10587
+ validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
10588
+ opts = parseCommonOptions(values, { runtimeDefaults: {
10589
+ leaseTtlSec: profile.leaseTtlSec,
10590
+ heartbeatIntervalMs: profile.heartbeatIntervalMs,
10591
+ maxBatchSize: profile.maxBatchSize,
10592
+ warmSessionTtlSec: resolveProfileWarmSessionTtlSec(profile)
10593
+ } });
10594
+ const sandbox = {
10562
10595
  config: profile.sandboxConfig,
10563
10596
  rootDir: profile.mountPath,
10564
10597
  path: profile.source
10565
- } : resolveSandbox(process.cwd(), values.sandbox);
10598
+ };
10566
10599
  const piAgentDir = ensurePiAgentDir(sandbox.rootDir, cfg.piCodingAgentDir);
10567
10600
  activatePiCodingAgentDir(piAgentDir.path);
10568
10601
  const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
10569
- const slotRegistry = createApiRuntimeSlotStore({
10570
- agent: ctx.agent,
10571
- daemonProfileId: profile?.id ?? null
10572
- });
10602
+ const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
10573
10603
  const slotIdentity = {
10574
10604
  agentName: opts.agent,
10575
- provider,
10576
- model
10605
+ runtimeProfileId: profile.id
10577
10606
  };
10578
- const mainRepo = findMainWorktree();
10579
10607
  const executionPlans = createExecutionPlanCache({
10580
10608
  stateDirs,
10581
10609
  slotIdentity,
@@ -10589,9 +10617,9 @@ async function runOnce(argv) {
10589
10617
  resourceAttributes: {
10590
10618
  "moltnet.task.id": taskId,
10591
10619
  "moltnet.agent.name": opts.agent,
10592
- "moltnet.llm.provider": provider,
10593
- "moltnet.llm.model": model,
10594
- ...profile ? { "moltnet.daemon_profile.id": profile.id } : {}
10620
+ "moltnet.llm.provider": profile.provider,
10621
+ "moltnet.llm.model": profile.model,
10622
+ "moltnet.runtime_profile.id": profile.id
10595
10623
  }
10596
10624
  });
10597
10625
  const { logger, shutdown: shutdownLogger } = createRootLogger({
@@ -10601,12 +10629,10 @@ async function runOnce(argv) {
10601
10629
  const rootLogger = logger.child({
10602
10630
  mode: "once",
10603
10631
  agent: opts.agent,
10604
- provider,
10605
- model,
10606
- ...profile ? {
10607
- daemonProfileId: profile.id,
10608
- daemonProfileName: profile.name
10609
- } : {}
10632
+ provider: profile.provider,
10633
+ model: profile.model,
10634
+ runtimeProfileId: profile.id,
10635
+ runtimeProfileName: profile.name
10610
10636
  });
10611
10637
  rootLogger.info({
10612
10638
  sandbox: sandbox.path,
@@ -10614,11 +10640,9 @@ async function runOnce(argv) {
10614
10640
  leaseTtlSec: opts.leaseTtlSec,
10615
10641
  heartbeatIntervalMs: opts.heartbeatIntervalMs,
10616
10642
  warmSessionTtlSec: opts.warmSessionTtlSec,
10617
- ...profile ? {
10618
- profileId: profile.id,
10619
- profileSessionTtlSec: profile.sessionTtlSec,
10620
- profileWorkspaceTtlSec: profile.workspaceTtlSec
10621
- } : {},
10643
+ profileId: profile.id,
10644
+ profileSessionTtlSec: profile.sessionTtlSec,
10645
+ profileWorkspaceTtlSec: profile.workspaceTtlSec,
10622
10646
  piAgentDir: piAgentDir.path,
10623
10647
  piAgentDirSource: piAgentDir.source
10624
10648
  }, "agent-daemon.starting");
@@ -10651,9 +10675,10 @@ async function runOnce(argv) {
10651
10675
  try {
10652
10676
  const rawExecuteTask = createPiTaskExecutor({
10653
10677
  agentName: opts.agent,
10678
+ agentRootDir: ctx.agentRootDir,
10654
10679
  mountPath: sandbox.rootDir,
10655
- provider,
10656
- model,
10680
+ provider: profile.provider,
10681
+ model: profile.model,
10657
10682
  sandboxConfig: sandbox.config,
10658
10683
  makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
10659
10684
  onTurnEvent: makeTurnEventHandler(rootLogger, { taskId }),
@@ -10691,13 +10716,16 @@ async function runOnce(argv) {
10691
10716
  }
10692
10717
  if (executionPlan.slotKey && executionPlan.sessionPersistence) await slotRegistry.beginSlot({
10693
10718
  ...slotIdentity,
10719
+ runtimeProfileId: profile.id,
10720
+ provider: profile.provider,
10721
+ model: profile.model,
10694
10722
  teamId: claimedTask.task.teamId,
10695
10723
  slotKey: executionPlan.slotKey,
10696
10724
  taskType: claimedTask.task.taskType,
10697
10725
  sessionDir: executionPlan.sessionPersistence.sessionDir,
10698
10726
  sessionPath: resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir),
10699
10727
  workspaceId: executionPlan.workspaceId,
10700
- worktreePath: resolveRecordedWorkspacePath(mainRepo, stateDirs.rootDir, executionPlan),
10728
+ worktreePath: resolveRecordedWorkspacePath(stateDirs.rootDir, executionPlan),
10701
10729
  worktreeBranch: executionPlan.worktreeBranch,
10702
10730
  workspaceKind: executionPlan.workspaceKind,
10703
10731
  lastTaskId: claimedTask.task.id,
@@ -10705,11 +10733,16 @@ async function runOnce(argv) {
10705
10733
  });
10706
10734
  activeAttemptN = claimedTask.attemptN;
10707
10735
  try {
10708
- return await rawExecuteTask(claimedTask, reporter);
10736
+ return await runWithDaemonRuntimeContext({
10737
+ profileId: profile.id,
10738
+ profileName: profile.name,
10739
+ provider: profile.provider,
10740
+ model: profile.model
10741
+ }, () => rawExecuteTask(claimedTask, reporter));
10709
10742
  } finally {
10710
10743
  activeAttemptN = null;
10711
10744
  executionPlans.delete(claimedTask);
10712
- if (executionPlan.slotKey) await slotRegistry.finishSlot(claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN, slotIdentity, executionPlan.slotKey, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null);
10745
+ 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);
10713
10746
  }
10714
10747
  };
10715
10748
  const writeCorrelationAnchors = makePrBodyAnchorWriter({
@@ -10722,7 +10755,7 @@ async function runOnce(argv) {
10722
10755
  agent: ctx.agent,
10723
10756
  taskId,
10724
10757
  leaseTtlSec: opts.leaseTtlSec,
10725
- ...profile ? { profileId: profile.id } : {}
10758
+ profileId: profile.id
10726
10759
  }),
10727
10760
  makeReporter: () => new ApiTaskReporter({
10728
10761
  tasks: ctx.agent.tasks,
@@ -10757,9 +10790,9 @@ async function runOnce(argv) {
10757
10790
  await shutdownLogger();
10758
10791
  }
10759
10792
  }
10760
- function resolveRecordedWorkspacePath(mainRepo, stateRootDir, executionPlan) {
10793
+ function resolveRecordedWorkspacePath(stateRootDir, executionPlan) {
10761
10794
  if (!executionPlan.workspaceId) return null;
10762
- return executionPlan.workspaceMode === "scratch_mount" ? join(stateRootDir, "task-workspaces", executionPlan.workspaceId) : join(mainRepo, ".worktrees", executionPlan.workspaceId);
10795
+ return executionPlan.workspaceMode === "scratch_mount" ? join(stateRootDir, "task-workspaces", executionPlan.workspaceId) : join(findMainWorktree(), ".worktrees", executionPlan.workspaceId);
10763
10796
  }
10764
10797
  //#endregion
10765
10798
  //#region src/cli/poll.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.20.0",
3
+ "version": "0.22.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,9 +45,9 @@
45
45
  "@opentelemetry/semantic-conventions": "^1.39.0",
46
46
  "pino": "^10.3.1",
47
47
  "pino-pretty": "^13.1.3",
48
- "@themoltnet/agent-runtime": "0.27.0",
49
- "@themoltnet/pi-extension": "0.26.1",
50
- "@themoltnet/sdk": "0.109.0"
48
+ "@themoltnet/sdk": "0.109.0",
49
+ "@themoltnet/pi-extension": "0.26.3",
50
+ "@themoltnet/agent-runtime": "0.28.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "tsx": "^4.7.0",
@@ -56,8 +56,8 @@
56
56
  "vitest": "^3.0.0",
57
57
  "@moltnet/bootstrap": "0.1.0",
58
58
  "@moltnet/crypto-service": "0.1.0",
59
- "@moltnet/observability": "0.1.0",
60
- "@moltnet/tasks": "0.1.0"
59
+ "@moltnet/tasks": "0.1.0",
60
+ "@moltnet/observability": "0.1.0"
61
61
  },
62
62
  "nx": {
63
63
  "tags": [