@themoltnet/agent-daemon 0.31.1 → 0.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +23 -2
  2. package/dist/main.js +488 -33
  3. package/package.json +6 -6
package/README.md CHANGED
@@ -156,8 +156,9 @@ registered task type; unknown task-type names remain invalid.
156
156
  provider key referenced by `.pi/models.json`, for example `OLLAMA_API_KEY`.
157
157
  - `ssh-keygen` on `PATH`.
158
158
  - A runtime profile in the target team. The profile supplies provider, model,
159
- sandbox policy, and runtime defaults. The daemon mounts the current working
160
- directory as the VM workspace root.
159
+ sandbox policy, and runtime defaults. The daemon resolves the configured
160
+ agent root and uses that checkout as the VM workspace root, regardless of
161
+ the shell directory from which the command was launched.
161
162
 
162
163
  For `themoltnet`, prefer a profile sandbox equivalent to this minimal policy:
163
164
 
@@ -203,6 +204,26 @@ producer context, the judge fails with `producer_context_missing`.
203
204
  Repo-specific `resumeCommands` that should not run in scratch mode must still
204
205
  be guarded with `when.workspaceMode`.
205
206
 
207
+ ### Runtime resource lifecycle
208
+
209
+ Each daemon process creates a unique runtime lane. Two polling processes using
210
+ the same agent, runtime profile, and task correlation therefore write to
211
+ different local Pi session directories and cannot race on the same slot.
212
+
213
+ `freeform` Pi context remains correlation-scoped, but its checkout is
214
+ attempt-scoped. Every attempt gets a fresh `daemon-task-<id>-attempt-<n>`
215
+ workspace; retries and explicit continuations fork the previous checkpointed
216
+ Pi session into that new workspace. The executor removes attempt workspaces on
217
+ normal completion. At startup, and once per minute while polling, the daemon
218
+ also reaps expired idle slots and terminal crash-orphans. Cleanup is restricted
219
+ to daemon-owned session, scratch, and `.worktrees` roots.
220
+
221
+ Provider failures are retried in the active Pi session before the daemon spends
222
+ a task attempt. The default is four same-session retries. If those fail,
223
+ deterministic retry classification runs before attempt-budget handling;
224
+ `executor_threw` is always treated as an implementation/setup failure and is
225
+ never promoted to another task attempt.
226
+
206
227
  ### 1. Start the local stack
207
228
 
208
229
  The e2e Compose file ships everything the daemon needs (Postgres, Ory, REST
package/dist/main.js CHANGED
@@ -12,8 +12,9 @@ import { parseArgs, promisify } from "node:util";
12
12
  import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource } from "@themoltnet/agent-runtime";
13
13
  import { createPiRetryTriage, createPiTaskExecutor, findMainWorktree, normalizeRetryTriageResult, redactRetryTriageSecrets } from "@themoltnet/pi-extension";
14
14
  import { execFile, execFileSync } from "node:child_process";
15
- import { accessSync, constants, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync } from "node:fs";
15
+ import { accessSync, constants, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync, realpathSync, rmSync } from "node:fs";
16
16
  import { AuthenticationError, MoltNetError, connect } from "@themoltnet/sdk";
17
+ import { createHash, randomUUID } from "node:crypto";
17
18
  import { once } from "node:events";
18
19
  import { pino, transport } from "pino";
19
20
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
@@ -25,7 +26,6 @@ import { AsyncLocalStorage } from "node:async_hooks";
25
26
  import { getModel } from "@earendil-works/pi-ai";
26
27
  import { mkdir, realpath, stat } from "node:fs/promises";
27
28
  import { pipeline } from "node:stream/promises";
28
- import { createHash } from "node:crypto";
29
29
  import { Writable } from "node:stream";
30
30
  import { createGzip } from "node:zlib";
31
31
  //#region ../../libs/observability/src/instrumentation.ts
@@ -4392,6 +4392,68 @@ _Object_({
4392
4392
  additionalProperties: false
4393
4393
  });
4394
4394
  //#endregion
4395
+ //#region ../../libs/tasks/src/runtime-profile-context-recipes.ts
4396
+ var RUNTIME_PROFILE_CONTEXT_CATALOGUE = {
4397
+ version: 1,
4398
+ fragments: {
4399
+ "accountable-delivery-v1": {
4400
+ binding: "prompt_prefix",
4401
+ content: "# Accountable delivery\n\n- Pair every commit made during this task with a signed diary entry created by the `moltnet_create_entry` custom tool. Put the returned id in a `MoltNet-Diary: <id>` commit trailer.\n- Keep commit signing enabled; do not bypass the agent git configuration.\n- Push a branch and open or update a pull request only when the task asks for it. For GitHub mutations, use the credential-bound `GH_TOKEN` command form required by the runtime kernel.\n- Keep changes, commits, and any requested pull request coherent enough to review independently.",
4402
+ slug: "accountable-delivery-v1"
4403
+ },
4404
+ "judgment-diary-v1": {
4405
+ binding: "prompt_prefix",
4406
+ content: "# Judgment diary discipline\n\n- For an `assess_brief`, `judge_pack`, or `pr_review` task, create a signed diary entry with the `moltnet_create_entry` custom tool before submitting the structured judgment. Capture the rationale and evidence that support the verdict.\n- Add the `judgment` tag and the active task type tag (`assess_brief`, `judge_pack`, or `pr_review`). For `judge_pack`, also add `rubric:<rubricId>` from the task facts.\n- Do not use a shell `moltnet entry` command: task provenance is injected only by the custom tool.",
4407
+ slug: "judgment-diary-v1"
4408
+ },
4409
+ "proactive-memory-v1": {
4410
+ binding: "prompt_prefix",
4411
+ content: "# Proactive memory use\n\n- Before non-trivial investigation, debugging, code changes, or review, check the task diary for relevant prior knowledge instead of waiting for a human to ask. Use `moltnet_diary_tags` for cheap reconnaissance, `moltnet_list_entries` when tags or task provenance are known, and `moltnet_search_entries` for semantic similarity. Do not search randomly: pass `taskFilter` for task-local or correlation-local queries, and pass `tags` / `entryTypes` for broader prior-knowledge queries using known tags such as `incident`, `decision`, or `scope:<area>`. Broaden only after constrained searches miss.\n- Before creating an `episodic` incident entry, search for similar incidents using the proposed title, root cause, error text, affected subsystem, and watch-for terms, filtered by `entryTypes: [\"episodic\", \"semantic\"]` and any known `scope:*` or task-provenance tags. If a close prior match exists, do not create an isolated duplicate: reference the prior entry in your response or diary content, update or link it when the new occurrence adds material evidence, or create a new recurrence entry only when the recurrence itself is important signal.\n- When you create a recurrence entry, include the prior matching entry id(s) in the content and explain what is new about this occurrence.",
4412
+ slug: "proactive-memory-v1"
4413
+ },
4414
+ "run-eval-direct-v1": {
4415
+ binding: "prompt_prefix",
4416
+ content: "# Direct evaluation run\n\nThe supplied scenario, typed task facts, injected context, and registered submit-output tool are the complete task contract. Do not search diaries, create diary entries, modify a repository, commit, branch, push, or open a pull request unless a task fact explicitly requires it. Submit the agent-authored payload in the first turn; correction turns exist only to recover a rejected or missing submission.",
4417
+ slug: "run-eval-direct-v1"
4418
+ },
4419
+ "task-diary-discipline-v1": {
4420
+ binding: "prompt_prefix",
4421
+ content: "# Task diary discipline\n\n- During a daemon task, create diary entries only through the `moltnet_create_entry` custom tool. It binds entries to the current task diary and injects task, type, attempt, and correlation provenance tags.\n- Do not shell out to `moltnet entry create`, `moltnet entry create-signed`, or any other `moltnet entry` subcommand from bash while a task is running. Those paths bypass the custom tool's task-tag injection, so task-filtered diary queries cannot find the entry.\n- You may add useful tags, but do not try to replace task provenance supplied by the runtime.",
4422
+ slug: "task-diary-discipline-v1"
4423
+ },
4424
+ "verification-and-artifacts-v1": {
4425
+ binding: "prompt_prefix",
4426
+ content: "# Verification and artifacts\n\n- Run relevant verification before submitting. When task facts include `successCriteria`, assess them honestly in the generated verification contract; a fail or skip with evidence is better than a fabricated pass.\n- The registered submit-output tool owns the exact agent submission schema and validation recovery. Use that schema; do not invent a JSON shape in prose.\n- Upload large files, binary files, logs, reports, screenshots, traces, bundles, or datasets before submitting. Include artifact metadata only where the typed submit contract permits it.\n- If the task depends on prior artifacts, list and download the exact referenced artifact before judging or continuing that work.",
4427
+ slug: "verification-and-artifacts-v1"
4428
+ }
4429
+ },
4430
+ recipes: {
4431
+ "run-eval-direct@v1": {
4432
+ description: "Minimal direct context for a short, isolated evaluation run.",
4433
+ fragments: ["run-eval-direct-v1"]
4434
+ },
4435
+ "standard-engineering@v1": {
4436
+ description: "Full opt-in operating guidance for engineering tasks that need diary research, accountable delivery, and verification discipline.",
4437
+ fragments: [
4438
+ "proactive-memory-v1",
4439
+ "task-diary-discipline-v1",
4440
+ "accountable-delivery-v1",
4441
+ "judgment-diary-v1",
4442
+ "verification-and-artifacts-v1"
4443
+ ]
4444
+ }
4445
+ }
4446
+ };
4447
+ function deepFreeze(value) {
4448
+ if (value && typeof value === "object") {
4449
+ for (const key of Object.keys(value)) deepFreeze(value[key]);
4450
+ Object.freeze(value);
4451
+ }
4452
+ return value;
4453
+ }
4454
+ deepFreeze(RUNTIME_PROFILE_CONTEXT_CATALOGUE);
4455
+ Object.freeze(Object.keys(RUNTIME_PROFILE_CONTEXT_CATALOGUE.recipes));
4456
+ //#endregion
4395
4457
  //#region ../../libs/tasks/src/runtime-profiles.ts
4396
4458
  var RuntimeProfileName = String$1({
4397
4459
  minLength: 1,
@@ -4413,6 +4475,16 @@ var RuntimeProfileWorkspaceMode = Union([
4413
4475
  Literal("shared_mount"),
4414
4476
  Literal("dedicated_worktree")
4415
4477
  ]);
4478
+ /**
4479
+ * Tool-policy enforcement mode for the profile's runtime `tool_call` gate:
4480
+ * `off` (inert), `watch` (audit only), `enforce` (block disallowed tools,
4481
+ * fail-closed). Read by the daemon via `GET /runtime-profiles/:id/allowed-tools`.
4482
+ */
4483
+ var RuntimeProfileToolEnforcement = Union([
4484
+ Literal("off"),
4485
+ Literal("watch"),
4486
+ Literal("enforce")
4487
+ ]);
4416
4488
  var RuntimeProfileAllowedWorkspaceModes = _Array_(RuntimeProfileWorkspaceMode, {
4417
4489
  minItems: 1,
4418
4490
  maxItems: 3,
@@ -4601,6 +4673,7 @@ _Object_({
4601
4673
  maxBatchSize: RuntimeProfileMaxBatchSize,
4602
4674
  maxTurns: RuntimeProfileMaxTurns,
4603
4675
  maxBashTimeouts: RuntimeProfileMaxBashTimeouts,
4676
+ toolEnforcement: RuntimeProfileToolEnforcement,
4604
4677
  requiredEnv: _Array_(RuntimeProfileEnvName, { maxItems: 100 }),
4605
4678
  requiredTools: _Array_(RuntimeProfileToolName, { maxItems: 100 }),
4606
4679
  context: _Array_(RuntimeProfileContext, { maxItems: 5 }),
@@ -6464,7 +6537,7 @@ var BUILT_IN_TASK_TYPES = {
6464
6537
  outputKind: "artifact",
6465
6538
  resumable: true,
6466
6539
  workspaceMode: "shared_mount",
6467
- workspaceScope: "session",
6540
+ workspaceScope: "attempt",
6468
6541
  sessionScope: "correlation",
6469
6542
  acceptsInputWorkspaceOverride: true,
6470
6543
  requiresReferences: false,
@@ -9191,8 +9264,6 @@ var TaskMessageKind = Union([
9191
9264
  var Uuid = String$1({ format: "uuid" });
9192
9265
  var Cid = String$1({ minLength: 1 });
9193
9266
  var IsoTimestamp = String$1({ format: "date-time" });
9194
- var MAX_CLAIM_CONDITION_BRANCHES = 8;
9195
- var MAX_CLAIM_CONDITION_STATUSES = 8;
9196
9267
  /**
9197
9268
  * Daemon-asserted runtime state stamped onto a `TaskAttemptSummary` at
9198
9269
  * attempt-completion time. The server persists this block verbatim and
@@ -9218,14 +9289,14 @@ Unsafe(Cyclic({ ClaimCondition: Unsafe(Union([
9218
9289
  op: Literal("all"),
9219
9290
  conditions: _Array_(Ref$1("ClaimCondition"), {
9220
9291
  minItems: 1,
9221
- maxItems: MAX_CLAIM_CONDITION_BRANCHES
9292
+ maxItems: 8
9222
9293
  })
9223
9294
  }, { additionalProperties: false }),
9224
9295
  _Object_({
9225
9296
  op: Literal("any"),
9226
9297
  conditions: _Array_(Ref$1("ClaimCondition"), {
9227
9298
  minItems: 1,
9228
- maxItems: MAX_CLAIM_CONDITION_BRANCHES
9299
+ maxItems: 8
9229
9300
  })
9230
9301
  }, { additionalProperties: false }),
9231
9302
  _Object_({
@@ -9233,7 +9304,7 @@ Unsafe(Cyclic({ ClaimCondition: Unsafe(Union([
9233
9304
  taskId: Uuid,
9234
9305
  statuses: _Array_(Ref$1("TaskStatus"), {
9235
9306
  minItems: 1,
9236
- maxItems: MAX_CLAIM_CONDITION_STATUSES
9307
+ maxItems: 8
9237
9308
  })
9238
9309
  }, { additionalProperties: false }),
9239
9310
  _Object_({
@@ -9809,6 +9880,11 @@ function createGhCliClient() {
9809
9880
  };
9810
9881
  }
9811
9882
  //#endregion
9883
+ //#region src/lib/daemon-slot-identity.ts
9884
+ function createRuntimeInstanceId() {
9885
+ return randomUUID();
9886
+ }
9887
+ //#endregion
9812
9888
  //#region src/lib/session-files.ts
9813
9889
  function resolveLatestPiSessionPath(sessionDir) {
9814
9890
  try {
@@ -9879,10 +9955,10 @@ function slugifySessionComponent(input) {
9879
9955
  }
9880
9956
  //#endregion
9881
9957
  //#region src/lib/task-execution-plan.ts
9882
- function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmSessionTtlSec, runtimeProfileWorkspacePolicy = {}) {
9958
+ function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmSessionTtlSec, runtimeProfileWorkspacePolicy = {}, attemptN) {
9883
9959
  const descriptor = deriveTaskSessionDescriptor(task);
9884
9960
  const workspaceMode = resolveTaskWorkspaceMode(task, descriptor.policy, runtimeProfileWorkspacePolicy);
9885
- const slotKey = warmSessionTtlSec > 0 ? descriptor.sessionKey : null;
9961
+ const slotKey = warmSessionTtlSec > 0 && descriptor.sessionKey ? buildRuntimeSlotKey(descriptor.sessionKey, identity.runtimeInstanceId) : null;
9886
9962
  const workspaceScope = slotKey !== null ? descriptor.policy.workspaceScope : "attempt";
9887
9963
  const slotId = slotKey ? buildDaemonSlotId(identity, slotKey) : null;
9888
9964
  const sessionDir = slotId ? `${stateDirs.piSessionsDir}/${encodeURIComponent(slotId)}` : null;
@@ -9890,7 +9966,8 @@ function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmSessionTtlS
9890
9966
  const workspaceId = workspaceMode !== "shared_mount" ? resolveTaskWorkspaceId(task, {
9891
9967
  sessionKey: slotId,
9892
9968
  workspaceScope,
9893
- sessionPersistence: sessionDir ? { sessionDir } : null
9969
+ sessionPersistence: sessionDir ? { sessionDir } : null,
9970
+ attemptN
9894
9971
  }) : null;
9895
9972
  return {
9896
9973
  descriptor,
@@ -9914,6 +9991,12 @@ function buildDaemonSlotId(identity, slotKey) {
9914
9991
  slotKey
9915
9992
  ].join(":");
9916
9993
  }
9994
+ function buildRuntimeSlotKey(logicalSessionKey, runtimeInstanceId) {
9995
+ return runtimeInstanceId ? `${logicalSessionKey}:worker:${slugSlotIdentityComponent(runtimeInstanceId)}` : logicalSessionKey;
9996
+ }
9997
+ function runtimeSlotKeyBelongsToInstance(slotKey, runtimeInstanceId) {
9998
+ return slotKey.endsWith(`:worker:${slugSlotIdentityComponent(runtimeInstanceId)}`);
9999
+ }
9917
10000
  function slugSlotIdentityComponent(input) {
9918
10001
  return slugifyAsciiLower(input.trim(), 64, [
9919
10002
  ".",
@@ -9968,7 +10051,7 @@ function toDaemonWorkspaceMode(mode) {
9968
10051
  }
9969
10052
  function resolveTaskWorkspaceId(task, executionPlan) {
9970
10053
  if (executionPlan.workspaceScope === "session" && executionPlan.sessionKey !== null) return `session-${encodeURIComponent(executionPlan.sessionKey)}`;
9971
- return `task-${task.id}`;
10054
+ return executionPlan.attemptN ? `daemon-task-${task.id}-attempt-${executionPlan.attemptN}` : `task-${task.id}`;
9972
10055
  }
9973
10056
  //#endregion
9974
10057
  //#region src/lib/execution-plan-cache.ts
@@ -9987,7 +10070,7 @@ function createExecutionPlanCache(args) {
9987
10070
  const key = buildClaimedTaskKey(claimedTask);
9988
10071
  const existing = cache.get(key);
9989
10072
  if (existing) return existing;
9990
- const plan = await maybeAttachWarmSlotContext(claimedTask, buildDaemonTaskExecutionPlan(claimedTask.task, args.stateDirs, args.slotIdentity, args.warmSessionTtlSec, args.workspacePolicy), args.stateDirs, args.slotRegistry, runtimeSessionStore, sourceAttemptResolver);
10073
+ const plan = await maybeAttachWarmSlotContext(claimedTask, buildDaemonTaskExecutionPlan(claimedTask.task, args.stateDirs, args.slotIdentity, args.warmSessionTtlSec, args.workspacePolicy, claimedTask.attemptN), args.stateDirs, args.slotRegistry, runtimeSessionStore, sourceAttemptResolver);
9991
10074
  assertPlanAllowedByWorkspacePolicy(plan, args.workspacePolicy);
9992
10075
  cache.set(key, plan);
9993
10076
  return plan;
@@ -10054,14 +10137,13 @@ async function resolveWarmSlot(slotRegistry, runtimeSessionStore, teamId, source
10054
10137
  return {
10055
10138
  kind: "found",
10056
10139
  producerSlot: producerContext,
10057
- sessionPath: sourceSessionPath,
10058
- workspacePath: resolveProducerWorkspaceCopySource(producerContext, stateDirs)
10140
+ sessionPath: sourceSessionPath
10059
10141
  };
10060
10142
  }
10061
10143
  async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slotRegistry, runtimeSessionStore, sourceAttemptResolver) {
10062
10144
  if (claimedTask.task.taskType === "freeform") {
10063
10145
  const continueFrom = claimedTask.task.input.continueFrom;
10064
- if (!continueFrom) return basePlan;
10146
+ if (!continueFrom) return maybeAttachRetrySession(claimedTask, basePlan, stateDirs, slotRegistry, runtimeSessionStore);
10065
10147
  const resolution = await resolveWarmSlot(slotRegistry, runtimeSessionStore, claimedTask.task.teamId, continueFrom.taskId, continueFrom.attemptN, stateDirs);
10066
10148
  if (resolution.kind === "missing") throw new ProducerContextResolutionError(`Continuation source task ${continueFrom.taskId} attempt ${continueFrom.attemptN} has no local runtime slot or durable runtime session — claim affinity filter should have prevented this claim`);
10067
10149
  if (resolution.kind === "no-session-path") throw new ProducerContextResolutionError(`Continuation source attempt ${continueFrom.taskId}/${continueFrom.attemptN} has no persisted Pi session path`);
@@ -10093,7 +10175,7 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
10093
10175
  return {
10094
10176
  ...basePlan,
10095
10177
  workspaceMode: "dedicated_worktree",
10096
- workspaceId: recoveredBranch ? `extend-${continueFrom.taskId}-attempt-${continueFrom.attemptN}` : null,
10178
+ workspaceId: recoveredBranch ? buildAttemptWorkspaceId(claimedTask) : null,
10097
10179
  worktreeBranch: recoveredBranch,
10098
10180
  sessionPersistence: {
10099
10181
  sessionDir,
@@ -10122,7 +10204,7 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
10122
10204
  return {
10123
10205
  ...basePlan,
10124
10206
  workspaceMode: "dedicated_worktree",
10125
- workspaceId: resolution.producerSlot.workspace?.workspaceId ?? null,
10207
+ workspaceId: parentBranch ? buildAttemptWorkspaceId(claimedTask) : null,
10126
10208
  worktreeBranch: parentBranch,
10127
10209
  sessionPersistence: {
10128
10210
  sessionDir,
@@ -10144,7 +10226,7 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
10144
10226
  worktreeBranch: null,
10145
10227
  workspaceKind: "scratch",
10146
10228
  workspaceSeed: {
10147
- copyFromPath: resolution.workspacePath,
10229
+ copyFromPath: resolveProducerWorkspaceCopySource(resolution.producerSlot, stateDirs),
10148
10230
  source: "producer"
10149
10231
  },
10150
10232
  sessionPersistence: {
@@ -10153,6 +10235,25 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
10153
10235
  }
10154
10236
  };
10155
10237
  }
10238
+ function buildAttemptWorkspaceId(claimedTask) {
10239
+ return `daemon-task-${claimedTask.task.id}-attempt-${claimedTask.attemptN}`;
10240
+ }
10241
+ async function maybeAttachRetrySession(claimedTask, basePlan, stateDirs, slotRegistry, runtimeSessionStore) {
10242
+ if (claimedTask.attemptN <= 1 || !basePlan.sessionPersistence?.sessionDir) return basePlan;
10243
+ const resolution = await resolveWarmSlot(slotRegistry, runtimeSessionStore, claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN - 1, stateDirs);
10244
+ if (resolution.kind === "missing" || resolution.kind === "no-session-path") return basePlan;
10245
+ const sourceSessionDir = dirname(resolution.sessionPath);
10246
+ const targetSessionDir = basePlan.sessionPersistence.sessionDir;
10247
+ if (sourceSessionDir === targetSessionDir) return basePlan;
10248
+ return {
10249
+ ...basePlan,
10250
+ worktreeBranch: basePlan.workspaceMode === "dedicated_worktree" && resolution.kind === "found" ? resolution.producerSlot.workspace?.worktreeBranch ?? basePlan.worktreeBranch : basePlan.worktreeBranch,
10251
+ sessionPersistence: {
10252
+ sessionDir: targetSessionDir,
10253
+ forkFromSessionPath: resolution.sessionPath
10254
+ }
10255
+ };
10256
+ }
10156
10257
  function buildForkBranch(parentBranch, childTaskId, childAttemptN) {
10157
10258
  return `${parentBranch}-fork-${childTaskId.slice(0, 8)}-${childAttemptN}`;
10158
10259
  }
@@ -10196,6 +10297,7 @@ var RETRYABLE_CODES = new Set([
10196
10297
  ]);
10197
10298
  var NON_RETRYABLE_CODES = new Set([
10198
10299
  "bad_api_key",
10300
+ "executor_threw",
10199
10301
  "invalid_api_key",
10200
10302
  "invalid_model",
10201
10303
  "max_turns_exceeded",
@@ -10238,17 +10340,18 @@ var NON_RETRYABLE_MESSAGE_PATTERNS = [
10238
10340
  /\bmax (?:turn|bash)/i
10239
10341
  ];
10240
10342
  async function classifyAttemptFailure(input) {
10241
- if (input.remainingAttempts !== null && input.remainingAttempts !== void 0) {
10242
- if (input.remainingAttempts <= 0) return {
10343
+ const deterministic = classifyDeterministically(input.error);
10344
+ if (input.remainingAttempts !== null && input.remainingAttempts !== void 0 && input.remainingAttempts <= 0) {
10345
+ const deterministicReason = deterministic === "ambiguous" ? "" : ` Deterministic policy classified the failure as ${deterministic}.`;
10346
+ return {
10243
10347
  error: withRetryInfo(input.error, {
10244
10348
  retryable: false,
10245
10349
  source: "attempts_exhausted",
10246
- reason: `Attempt budget exhausted at attempt ${input.attemptN}${input.maxAttempts ? ` of ${input.maxAttempts}` : ""}.`
10350
+ reason: `Attempt budget exhausted at attempt ${input.attemptN}${input.maxAttempts ? ` of ${input.maxAttempts}` : ""}.${deterministicReason}`
10247
10351
  }),
10248
10352
  source: "attempts_exhausted"
10249
10353
  };
10250
10354
  }
10251
- const deterministic = classifyDeterministically(input.error);
10252
10355
  if (deterministic !== "ambiguous") {
10253
10356
  const retryable = deterministic === "retryable";
10254
10357
  const source = input.error.retryable === retryable ? "explicit" : "deterministic";
@@ -10750,6 +10853,7 @@ async function resolveRuntimeProfile(options) {
10750
10853
  allowedWorkspaceModes: profile.allowedWorkspaceModes,
10751
10854
  requiredEnv: profile.requiredEnv,
10752
10855
  requiredTools: profile.requiredTools,
10856
+ toolEnforcement: profile.toolEnforcement,
10753
10857
  context: profile.context ?? [],
10754
10858
  sandboxConfig: profile.sandbox,
10755
10859
  mountPath: resolve(options.cwd),
@@ -10823,6 +10927,269 @@ function createRuntimeProfileRetryTriage(options) {
10823
10927
  });
10824
10928
  }
10825
10929
  //#endregion
10930
+ //#region src/lib/runtime-resource-reaper.ts
10931
+ var RUNTIME_SLOT_PAGE_LIMIT = 200;
10932
+ var ATTEMPT_LOOKUP_CONCURRENCY = 8;
10933
+ var TERMINAL_ATTEMPT_STATUSES = new Set([
10934
+ "completed",
10935
+ "failed",
10936
+ "cancelled",
10937
+ "aborted",
10938
+ "timed_out"
10939
+ ]);
10940
+ /**
10941
+ * Reap daemon-owned local resources whose runtime slot is no longer usable.
10942
+ * The destructive path is deliberately conservative: uncertainty about slot
10943
+ * ownership, attempt state, listing completeness, or real filesystem
10944
+ * containment retains resources rather than risking another daemon's data.
10945
+ */
10946
+ async function reapRuntimeSlotResources(deps, input) {
10947
+ const now = input.now ?? Date.now();
10948
+ const [activePage, idlePage] = await Promise.all([listOwnedSlots(deps.runtimeSlotStore, input, "active"), listOwnedSlots(deps.runtimeSlotStore, input, "idle")]);
10949
+ const active = activePage.items;
10950
+ const idle = idlePage.items;
10951
+ const slots = [...active, ...idle];
10952
+ const result = {
10953
+ failed: 0,
10954
+ failures: [],
10955
+ removedSessions: 0,
10956
+ removedWorkspaces: 0,
10957
+ scanned: slots.length,
10958
+ truncated: activePage.truncated || idlePage.truncated,
10959
+ unsafePathDetails: [],
10960
+ unsafePaths: 0
10961
+ };
10962
+ const reaped = new Set(idle.filter((item) => item.slot.expiresAtMs <= now).map((item) => item.slot.id));
10963
+ for (const slotId of await findTerminalActiveSlotIds(deps, active, result)) reaped.add(slotId);
10964
+ const retainedSessionDirs = new Set(slots.filter((item) => !reaped.has(item.slot.id)).flatMap((item) => item.session?.sessionDir ? [resolve(item.session.sessionDir)] : []));
10965
+ const retainedWorkspacePaths = new Set(slots.filter((item) => !reaped.has(item.slot.id)).flatMap((item) => item.workspace?.worktreePath ? [resolve(item.workspace.worktreePath)] : []));
10966
+ const worktrees = input.mainWorktree ? await listRegisteredWorktrees(input.mainWorktree) : {
10967
+ kind: "ready",
10968
+ paths: /* @__PURE__ */ new Set()
10969
+ };
10970
+ for (const item of slots) {
10971
+ if (!reaped.has(item.slot.id)) continue;
10972
+ if (!await slotStillQualifiesForReaping(deps, input, item, now, result)) continue;
10973
+ const sessionDir = item.session?.sessionDir;
10974
+ if (sessionDir && !retainedSessionDirs.has(resolve(sessionDir)) && existsSync(sessionDir)) if (!isRealPathInsideRoot(sessionDir, input.sessionRootDir)) recordUnsafePath(deps, result, item.slot.id, sessionDir, input.sessionRootDir);
10975
+ else try {
10976
+ rmSync(sessionDir, {
10977
+ force: true,
10978
+ recursive: true
10979
+ });
10980
+ result.removedSessions++;
10981
+ } catch (err) {
10982
+ recordFailure(deps, result, item.slot.id, sessionDir, input.sessionRootDir, err);
10983
+ }
10984
+ const workspacePath = item.workspace?.worktreePath;
10985
+ if (!workspacePath || retainedWorkspacePaths.has(resolve(workspacePath))) continue;
10986
+ const workspaceRoot = item.workspace?.kind === "scratch" ? input.scratchRootDir : input.mainWorktree ? resolve(input.mainWorktree, ".worktrees") : null;
10987
+ if (!workspaceRoot) {
10988
+ recordUnsafePath(deps, result, item.slot.id, workspacePath, null);
10989
+ continue;
10990
+ }
10991
+ await removeWorkspace(deps, result, worktrees, input.mainWorktree, item.slot.id, workspacePath, workspaceRoot, item.workspace?.kind === "scratch");
10992
+ }
10993
+ if (!result.truncated) await reapLegacyOrphanWorktrees(deps, input, worktrees, retainedSessionDirs, retainedWorkspacePaths, result);
10994
+ return result;
10995
+ }
10996
+ async function listOwnedSlots(store, input, state) {
10997
+ const listed = await store.listSlots({
10998
+ agentName: input.agentName,
10999
+ limit: RUNTIME_SLOT_PAGE_LIMIT,
11000
+ runtimeProfileId: input.runtimeProfileId,
11001
+ state,
11002
+ teamId: input.teamId
11003
+ });
11004
+ return {
11005
+ items: listed.filter((item) => runtimeSlotKeyBelongsToInstance(item.slot.slotKey, input.runtimeInstanceId)),
11006
+ truncated: listed.length === RUNTIME_SLOT_PAGE_LIMIT
11007
+ };
11008
+ }
11009
+ async function findTerminalActiveSlotIds(deps, active, result) {
11010
+ const byTask = /* @__PURE__ */ new Map();
11011
+ for (const item of active) {
11012
+ const taskSlots = byTask.get(item.slot.lastTaskId) ?? [];
11013
+ taskSlots.push(item);
11014
+ byTask.set(item.slot.lastTaskId, taskSlots);
11015
+ }
11016
+ const terminal = /* @__PURE__ */ new Set();
11017
+ await mapWithConcurrency([...byTask.entries()], ATTEMPT_LOOKUP_CONCURRENCY, async ([taskId, taskSlots]) => {
11018
+ let attempts;
11019
+ try {
11020
+ attempts = await deps.taskReader.listAttempts(taskId);
11021
+ } catch (err) {
11022
+ recordFailure(deps, result, null, `task:${taskId}`, null, err);
11023
+ return;
11024
+ }
11025
+ for (const item of taskSlots) {
11026
+ const attempt = attempts.find((candidate) => candidate.attemptN === item.slot.lastAttemptN);
11027
+ if (attempt && TERMINAL_ATTEMPT_STATUSES.has(attempt.status)) terminal.add(item.slot.id);
11028
+ }
11029
+ });
11030
+ return terminal;
11031
+ }
11032
+ async function slotStillQualifiesForReaping(deps, input, original, now, result) {
11033
+ let page;
11034
+ try {
11035
+ page = await listOwnedSlots(deps.runtimeSlotStore, input, original.slot.state);
11036
+ } catch (err) {
11037
+ recordFailure(deps, result, original.slot.id, `slot:${original.slot.id}`, null, err);
11038
+ return false;
11039
+ }
11040
+ const current = page.items.find((candidate) => candidate.slot.id === original.slot.id);
11041
+ if (!current) return false;
11042
+ if (current.slot.lastTaskId !== original.slot.lastTaskId || current.slot.lastAttemptN !== original.slot.lastAttemptN) return false;
11043
+ return current.slot.state === "active" || current.slot.state === "idle" && current.slot.expiresAtMs <= now;
11044
+ }
11045
+ async function reapLegacyOrphanWorktrees(deps, input, worktrees, retainedSessionDirs, retainedWorkspacePaths, result) {
11046
+ if (!input.mainWorktree) return;
11047
+ const workspaceRoot = resolve(input.mainWorktree, ".worktrees");
11048
+ if (!existsSync(workspaceRoot)) return;
11049
+ let entries;
11050
+ try {
11051
+ entries = readdirSync(workspaceRoot, { withFileTypes: true });
11052
+ } catch (err) {
11053
+ recordFailure(deps, result, null, workspaceRoot, workspaceRoot, err);
11054
+ return;
11055
+ }
11056
+ for (const entry of entries) {
11057
+ if (!entry.isDirectory() || !entry.name.startsWith("session-agent%3A")) continue;
11058
+ const workspacePath = resolve(workspaceRoot, entry.name);
11059
+ if (retainedWorkspacePaths.has(workspacePath)) continue;
11060
+ const sessionDir = resolve(input.sessionRootDir, entry.name.slice(8));
11061
+ if (retainedSessionDirs.has(sessionDir) || existsSync(sessionDir)) continue;
11062
+ await removeWorkspace(deps, result, worktrees, input.mainWorktree, null, workspacePath, workspaceRoot, false);
11063
+ }
11064
+ }
11065
+ async function removeWorkspace(deps, result, worktrees, mainWorktree, slotId, workspacePath, workspaceRoot, scratch) {
11066
+ if (!existsSync(workspacePath)) return;
11067
+ if (!isRealPathInsideRoot(workspacePath, workspaceRoot)) {
11068
+ recordUnsafePath(deps, result, slotId, workspacePath, workspaceRoot);
11069
+ return;
11070
+ }
11071
+ if (scratch || !mainWorktree) {
11072
+ try {
11073
+ rmSync(workspacePath, {
11074
+ force: true,
11075
+ recursive: true
11076
+ });
11077
+ result.removedWorkspaces++;
11078
+ } catch (err) {
11079
+ recordFailure(deps, result, slotId, workspacePath, workspaceRoot, err);
11080
+ }
11081
+ return;
11082
+ }
11083
+ if (worktrees.kind === "unavailable") {
11084
+ recordFailure(deps, result, slotId, workspacePath, workspaceRoot, new Error(worktrees.reason));
11085
+ return;
11086
+ }
11087
+ try {
11088
+ const canonical = realpathSync(workspacePath);
11089
+ if (worktrees.paths.has(canonical)) {
11090
+ await execFileText("git", [
11091
+ "-C",
11092
+ mainWorktree,
11093
+ "worktree",
11094
+ "remove",
11095
+ "--force",
11096
+ workspacePath
11097
+ ]);
11098
+ worktrees.paths.delete(canonical);
11099
+ } else rmSync(workspacePath, {
11100
+ force: true,
11101
+ recursive: true
11102
+ });
11103
+ result.removedWorkspaces++;
11104
+ } catch (err) {
11105
+ recordFailure(deps, result, slotId, workspacePath, workspaceRoot, err);
11106
+ }
11107
+ }
11108
+ async function listRegisteredWorktrees(mainWorktree) {
11109
+ try {
11110
+ const list = await execFileText("git", [
11111
+ "-C",
11112
+ mainWorktree,
11113
+ "worktree",
11114
+ "list",
11115
+ "--porcelain"
11116
+ ]);
11117
+ return {
11118
+ kind: "ready",
11119
+ paths: new Set(list.split("\n").filter((line) => line.startsWith("worktree ")).map((line) => canonicalPath(line.slice(9))))
11120
+ };
11121
+ } catch (err) {
11122
+ return {
11123
+ kind: "unavailable",
11124
+ reason: errorMessage(err)
11125
+ };
11126
+ }
11127
+ }
11128
+ function isRealPathInsideRoot(path, root) {
11129
+ try {
11130
+ return isResolvedPathInsideRoot$1(realpathSync(path), realpathSync(root));
11131
+ } catch {
11132
+ return false;
11133
+ }
11134
+ }
11135
+ function isResolvedPathInsideRoot$1(path, root) {
11136
+ const rel = relative(root, path);
11137
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
11138
+ }
11139
+ function canonicalPath(path) {
11140
+ try {
11141
+ return realpathSync(path);
11142
+ } catch {
11143
+ return resolve(path);
11144
+ }
11145
+ }
11146
+ function recordUnsafePath(deps, result, slotId, path, root) {
11147
+ const issue = {
11148
+ kind: "unsafe_path",
11149
+ path,
11150
+ reason: root === null ? "No daemon-owned root was available for this workspace." : "Candidate or root could not be resolved inside the daemon-owned root.",
11151
+ root,
11152
+ slotId
11153
+ };
11154
+ result.unsafePaths++;
11155
+ result.unsafePathDetails.push(issue);
11156
+ emitIssue(deps, issue);
11157
+ }
11158
+ function recordFailure(deps, result, slotId, path, root, err) {
11159
+ const issue = {
11160
+ kind: "failure",
11161
+ path,
11162
+ reason: errorMessage(err),
11163
+ root,
11164
+ slotId
11165
+ };
11166
+ result.failed++;
11167
+ result.failures.push(issue);
11168
+ emitIssue(deps, issue);
11169
+ }
11170
+ function emitIssue(deps, issue) {
11171
+ try {
11172
+ deps.onIssue?.(issue);
11173
+ } catch {}
11174
+ }
11175
+ function errorMessage(err) {
11176
+ return err instanceof Error ? err.message : String(err);
11177
+ }
11178
+ async function execFileText(file, args) {
11179
+ return new Promise((resolvePromise, reject) => {
11180
+ execFile(file, args, {
11181
+ encoding: "utf8",
11182
+ maxBuffer: 10 * 1024 * 1024
11183
+ }, (err, stdout) => {
11184
+ if (err) reject(err instanceof Error ? err : /* @__PURE__ */ new Error("Child process failed without an Error instance."));
11185
+ else resolvePromise(stdout);
11186
+ });
11187
+ });
11188
+ }
11189
+ async function mapWithConcurrency(items, concurrency, worker) {
11190
+ for (let index = 0; index < items.length; index += concurrency) await Promise.all(items.slice(index, index + concurrency).map(worker));
11191
+ }
11192
+ //#endregion
10826
11193
  //#region src/lib/runtime-sessions.ts
10827
11194
  function resolveRuntimeSessionKind(claimedTask) {
10828
11195
  const continueFrom = resolveContinueFrom(claimedTask);
@@ -10959,6 +11326,8 @@ function createApiRuntimeSlotStore(args) {
10959
11326
  lastAttemptN: resolved.slot.lastAttemptN,
10960
11327
  lastTaskId: resolved.slot.lastTaskId,
10961
11328
  runtimeProfileId: resolved.slot.runtimeProfileId,
11329
+ slotKey: resolved.slot.slotKey,
11330
+ state: resolved.slot.state,
10962
11331
  taskType: resolved.slot.taskType
10963
11332
  },
10964
11333
  session: resolved.slot.sessionDir ? {
@@ -11130,12 +11499,13 @@ async function runPolling(opts) {
11130
11499
  agent: ctx.agent,
11131
11500
  profiles: profileValues,
11132
11501
  teamId,
11133
- cwd: process.cwd()
11502
+ cwd: ctx.agentRootDir
11134
11503
  });
11135
11504
  for (const profile of profiles) validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
11136
11505
  const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
11137
11506
  const runtimeSessionStore = createApiRuntimeSessionStore({ agent: ctx.agent });
11138
11507
  const sourceAttemptResolver = createApiSourceAttemptResolver({ agent: ctx.agent });
11508
+ const runtimeInstanceId = createRuntimeInstanceId();
11139
11509
  const runtimes = /* @__PURE__ */ new Map();
11140
11510
  for (const profile of profiles) {
11141
11511
  const common = parseCommonOptions(values, { runtimeDefaults: {
@@ -11155,7 +11525,8 @@ async function runPolling(opts) {
11155
11525
  const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
11156
11526
  const slotIdentity = {
11157
11527
  agentName: common.agent,
11158
- runtimeProfileId: profile.id
11528
+ runtimeProfileId: profile.id,
11529
+ runtimeInstanceId
11159
11530
  };
11160
11531
  const executionPlans = createExecutionPlanCache({
11161
11532
  stateDirs,
@@ -11263,6 +11634,47 @@ async function runPolling(opts) {
11263
11634
  piAgentDir: piAgentDir.path,
11264
11635
  piAgentDirSource: piAgentDir.source
11265
11636
  }, "agent-daemon.starting");
11637
+ let reaperRunning = false;
11638
+ const reapRuntimeResources = async () => {
11639
+ if (reaperRunning) return;
11640
+ reaperRunning = true;
11641
+ try {
11642
+ for (const profile of profiles) {
11643
+ const selected = requireRuntime(runtimes, profile.id);
11644
+ const result = await reapRuntimeSlotResources({
11645
+ onIssue: (issue) => {
11646
+ rootLogger.warn({
11647
+ ...issue,
11648
+ runtimeProfileId: profile.id
11649
+ }, "agent-daemon.runtime_resource_reap_issue");
11650
+ },
11651
+ runtimeSlotStore: slotRegistry,
11652
+ taskReader: ctx.agent.tasks
11653
+ }, {
11654
+ agentName: selected.common.agent,
11655
+ mainWorktree: resolveMainWorktree$1(selected.sandbox.rootDir),
11656
+ runtimeInstanceId,
11657
+ runtimeProfileId: profile.id,
11658
+ sessionRootDir: selected.stateDirs.piSessionsDir,
11659
+ scratchRootDir: join(selected.stateDirs.rootDir, "task-workspaces"),
11660
+ teamId
11661
+ });
11662
+ if (result.removedSessions > 0 || result.removedWorkspaces > 0 || result.failed > 0 || result.unsafePaths > 0 || result.truncated) rootLogger.info({
11663
+ ...result,
11664
+ runtimeProfileId: profile.id
11665
+ }, "agent-daemon.runtime_resources_reaped");
11666
+ }
11667
+ } catch (err) {
11668
+ rootLogger.warn({ err }, "agent-daemon.runtime_resource_reap_failed");
11669
+ } finally {
11670
+ reaperRunning = false;
11671
+ }
11672
+ };
11673
+ await reapRuntimeResources();
11674
+ const reaperTimer = setInterval(() => {
11675
+ reapRuntimeResources();
11676
+ }, 6e4);
11677
+ reaperTimer.unref();
11266
11678
  const outputs = [];
11267
11679
  try {
11268
11680
  runtime = new AgentRuntime({
@@ -11440,7 +11852,7 @@ async function runPolling(opts) {
11440
11852
  sessionDir: executionPlan.sessionPersistence.sessionDir,
11441
11853
  sessionPath: resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir),
11442
11854
  workspaceId: executionPlan.workspaceId,
11443
- worktreePath: resolveRecordedWorkspacePath$1(stateDirs.rootDir, executionPlan),
11855
+ worktreePath: resolveRecordedWorkspacePath$1(stateDirs.rootDir, sandbox.rootDir, executionPlan),
11444
11856
  worktreeBranch: executionPlan.worktreeBranch,
11445
11857
  workspaceKind: executionPlan.workspaceKind,
11446
11858
  lastTaskId: claimedTask.task.id,
@@ -11460,8 +11872,11 @@ async function runPolling(opts) {
11460
11872
  sandboxConfig: sandbox.config,
11461
11873
  forwardEnv: profile.requiredEnv,
11462
11874
  runtimeProfileContext: profile.context,
11875
+ runtimeProfileId: profile.id,
11876
+ toolEnforcement: profile.toolEnforcement,
11463
11877
  makeExecutionPlan: (task) => executionPlans.getOrCreate(task),
11464
11878
  makeOnTurnEvent: makeTurnEventHandlerFactory(taskLogger),
11879
+ toolPolicyLogger: taskLogger,
11465
11880
  maxTurns: common.maxTurns,
11466
11881
  maxBashTimeouts: common.maxBashTimeouts
11467
11882
  });
@@ -11493,15 +11908,23 @@ async function runPolling(opts) {
11493
11908
  rootLogger.info({ processed: drained.length }, "agent-daemon.drained");
11494
11909
  return drained.some((o) => o.status !== "completed") ? 1 : 0;
11495
11910
  } finally {
11911
+ clearInterval(reaperTimer);
11496
11912
  signalHandlers.dispose();
11497
11913
  await slotRegistry.close();
11498
11914
  await otelShutdown();
11499
11915
  await shutdownLogger();
11500
11916
  }
11501
11917
  }
11502
- function resolveRecordedWorkspacePath$1(stateRootDir, executionPlan) {
11918
+ function resolveRecordedWorkspacePath$1(stateRootDir, mountPath, executionPlan) {
11503
11919
  if (!executionPlan.workspaceId) return null;
11504
- return executionPlan.workspaceMode === "scratch_mount" ? join(stateRootDir, "task-workspaces", executionPlan.workspaceId) : join(findMainWorktree(), ".worktrees", executionPlan.workspaceId);
11920
+ return executionPlan.workspaceMode === "scratch_mount" ? join(stateRootDir, "task-workspaces", executionPlan.workspaceId) : join(findMainWorktree(mountPath), ".worktrees", executionPlan.workspaceId);
11921
+ }
11922
+ function resolveMainWorktree$1(mountPath) {
11923
+ try {
11924
+ return findMainWorktree(mountPath);
11925
+ } catch {
11926
+ return null;
11927
+ }
11505
11928
  }
11506
11929
  function runtimeForClaimedTask(runtimes, claimedTask) {
11507
11930
  if (!claimedTask.profileId) throw new Error(`Claimed task ${claimedTask.task.id} did not include a selected runtime profile`);
@@ -11593,7 +12016,7 @@ async function runOnce(argv) {
11593
12016
  agent: ctx.agent,
11594
12017
  profile: values.profile,
11595
12018
  teamId: values.team,
11596
- cwd: process.cwd()
12019
+ cwd: ctx.agentRootDir
11597
12020
  });
11598
12021
  validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
11599
12022
  opts = parseCommonOptions(values, { runtimeDefaults: {
@@ -11615,9 +12038,11 @@ async function runOnce(argv) {
11615
12038
  const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
11616
12039
  const runtimeSessionStore = createApiRuntimeSessionStore({ agent: ctx.agent });
11617
12040
  const sourceAttemptResolver = createApiSourceAttemptResolver({ agent: ctx.agent });
12041
+ const runtimeInstanceId = createRuntimeInstanceId();
11618
12042
  const slotIdentity = {
11619
12043
  agentName: opts.agent,
11620
- runtimeProfileId: profile.id
12044
+ runtimeProfileId: profile.id,
12045
+ runtimeInstanceId
11621
12046
  };
11622
12047
  const executionPlans = createExecutionPlanCache({
11623
12048
  stateDirs,
@@ -11679,6 +12104,26 @@ async function runOnce(argv) {
11679
12104
  piAgentDir: piAgentDir.path,
11680
12105
  piAgentDirSource: piAgentDir.source
11681
12106
  }, "agent-daemon.starting");
12107
+ try {
12108
+ const reaped = await reapRuntimeSlotResources({
12109
+ onIssue: (issue) => {
12110
+ rootLogger.warn(issue, "agent-daemon.runtime_resource_reap_issue");
12111
+ },
12112
+ runtimeSlotStore: slotRegistry,
12113
+ taskReader: ctx.agent.tasks
12114
+ }, {
12115
+ agentName: opts.agent,
12116
+ mainWorktree: resolveMainWorktree(sandbox.rootDir),
12117
+ runtimeInstanceId,
12118
+ runtimeProfileId: profile.id,
12119
+ sessionRootDir: stateDirs.piSessionsDir,
12120
+ scratchRootDir: join(stateDirs.rootDir, "task-workspaces"),
12121
+ teamId: profile.teamId
12122
+ });
12123
+ if (reaped.removedSessions > 0 || reaped.removedWorkspaces > 0 || reaped.failed > 0 || reaped.unsafePaths > 0 || reaped.truncated) rootLogger.info(reaped, "agent-daemon.runtime_resources_reaped");
12124
+ } catch (err) {
12125
+ rootLogger.warn({ err }, "agent-daemon.runtime_resource_reap_failed");
12126
+ }
11682
12127
  let runtime = null;
11683
12128
  let activeAttemptN = null;
11684
12129
  const signalHandlers = installShutdownSignalHandlers({
@@ -11720,8 +12165,11 @@ async function runOnce(argv) {
11720
12165
  sandboxConfig: sandbox.config,
11721
12166
  forwardEnv: profile.requiredEnv,
11722
12167
  runtimeProfileContext: profile.context,
12168
+ runtimeProfileId: profile.id,
12169
+ toolEnforcement: profile.toolEnforcement,
11723
12170
  makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
11724
12171
  onTurnEvent: makeTurnEventHandler(rootLogger, { taskId }),
12172
+ toolPolicyLogger: rootLogger,
11725
12173
  maxTurns: opts.maxTurns,
11726
12174
  maxBashTimeouts: opts.maxBashTimeouts
11727
12175
  });
@@ -11765,7 +12213,7 @@ async function runOnce(argv) {
11765
12213
  sessionDir: executionPlan.sessionPersistence.sessionDir,
11766
12214
  sessionPath: resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir),
11767
12215
  workspaceId: executionPlan.workspaceId,
11768
- worktreePath: resolveRecordedWorkspacePath(stateDirs.rootDir, executionPlan),
12216
+ worktreePath: resolveRecordedWorkspacePath(stateDirs.rootDir, sandbox.rootDir, executionPlan),
11769
12217
  worktreeBranch: executionPlan.worktreeBranch,
11770
12218
  workspaceKind: executionPlan.workspaceKind,
11771
12219
  lastTaskId: claimedTask.task.id,
@@ -11861,9 +12309,16 @@ async function runOnce(argv) {
11861
12309
  await shutdownLogger();
11862
12310
  }
11863
12311
  }
11864
- function resolveRecordedWorkspacePath(stateRootDir, executionPlan) {
12312
+ function resolveRecordedWorkspacePath(stateRootDir, mountPath, executionPlan) {
11865
12313
  if (!executionPlan.workspaceId) return null;
11866
- return executionPlan.workspaceMode === "scratch_mount" ? join(stateRootDir, "task-workspaces", executionPlan.workspaceId) : join(findMainWorktree(), ".worktrees", executionPlan.workspaceId);
12314
+ return executionPlan.workspaceMode === "scratch_mount" ? join(stateRootDir, "task-workspaces", executionPlan.workspaceId) : join(findMainWorktree(mountPath), ".worktrees", executionPlan.workspaceId);
12315
+ }
12316
+ function resolveMainWorktree(mountPath) {
12317
+ try {
12318
+ return findMainWorktree(mountPath);
12319
+ } catch {
12320
+ return null;
12321
+ }
11867
12322
  }
11868
12323
  //#endregion
11869
12324
  //#region src/cli/poll.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/agent-daemon",
3
- "version": "0.31.1",
3
+ "version": "0.32.1",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "description": "MoltNet agent daemon — claims and executes tasks (fulfill_brief, assess_brief) from the MoltNet task-service via Pi-headless. CLI: moltnet-agent.",
@@ -51,9 +51,9 @@
51
51
  "@opentelemetry/semantic-conventions": "^1.39.0",
52
52
  "pino": "^10.3.1",
53
53
  "pino-pretty": "^13.1.3",
54
- "@themoltnet/agent-runtime": "0.36.5",
55
- "@themoltnet/sdk": "0.126.0",
56
- "@themoltnet/pi-extension": "0.35.5"
54
+ "@themoltnet/agent-runtime": "0.36.6",
55
+ "@themoltnet/sdk": "0.127.0",
56
+ "@themoltnet/pi-extension": "0.36.1"
57
57
  },
58
58
  "devDependencies": {
59
59
  "tsx": "^4.7.0",
@@ -61,9 +61,9 @@
61
61
  "vite": "^8.0.0",
62
62
  "vitest": "^3.0.0",
63
63
  "@moltnet/bootstrap": "0.1.0",
64
+ "@moltnet/crypto-service": "0.1.0",
64
65
  "@moltnet/tasks": "0.1.0",
65
- "@moltnet/observability": "0.1.0",
66
- "@moltnet/crypto-service": "0.1.0"
66
+ "@moltnet/observability": "0.1.0"
67
67
  },
68
68
  "nx": {
69
69
  "projectType": "application",