@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.
- package/README.md +23 -2
- package/dist/main.js +488 -33
- 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
|
|
160
|
-
|
|
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: "
|
|
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:
|
|
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:
|
|
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:
|
|
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 ?
|
|
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:
|
|
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.
|
|
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
|
-
|
|
10242
|
-
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
55
|
-
"@themoltnet/sdk": "0.
|
|
56
|
-
"@themoltnet/pi-extension": "0.
|
|
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",
|