@themoltnet/agent-daemon 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -8
- package/dist/main.js +305 -53
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -179,14 +179,13 @@ This matters for evals in particular. `run_eval` tasks declare their intended
|
|
|
179
179
|
workspace shape in `input.execution.workspace`: `none` becomes a
|
|
180
180
|
`scratch_mount`, `shared_mount` uses the daemon mount, and
|
|
181
181
|
`dedicated_worktree` uses an isolated checkout. Downstream
|
|
182
|
-
`judge_eval_attempt` tasks
|
|
183
|
-
session
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
producer
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
with `when.workspaceMode`.
|
|
182
|
+
`judge_eval_attempt` tasks can hydrate the producer Pi session from durable
|
|
183
|
+
runtime-session storage when producer slot/workspace metadata is available but
|
|
184
|
+
the local session file is unavailable. Workspace copying still depends on
|
|
185
|
+
producer slot/workspace metadata; if the daemon cannot resolve the required
|
|
186
|
+
producer context, the judge fails with `producer_context_missing`.
|
|
187
|
+
Repo-specific `resumeCommands` that should not run in scratch mode must still
|
|
188
|
+
be guarded with `when.workspaceMode`.
|
|
190
189
|
|
|
191
190
|
### 1. Start the local stack
|
|
192
191
|
|
package/dist/main.js
CHANGED
|
@@ -12,7 +12,7 @@ import { parseArgs, promisify } from "node:util";
|
|
|
12
12
|
import { AgentRuntime, ApiTaskReporter, ApiTaskSource, PollingApiTaskSource } from "@themoltnet/agent-runtime";
|
|
13
13
|
import { createPiTaskExecutor, findMainWorktree } from "@themoltnet/pi-extension";
|
|
14
14
|
import { execFile, execFileSync } from "node:child_process";
|
|
15
|
-
import { accessSync, constants, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
15
|
+
import { accessSync, constants, createReadStream, createWriteStream, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
16
16
|
import { MoltNetError, connect } from "@themoltnet/sdk";
|
|
17
17
|
import { once } from "node:events";
|
|
18
18
|
import { pino, transport } from "pino";
|
|
@@ -22,6 +22,8 @@ import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
|
22
22
|
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
23
23
|
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
|
24
24
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
25
|
+
import { mkdir } from "node:fs/promises";
|
|
26
|
+
import { pipeline } from "node:stream/promises";
|
|
25
27
|
//#region ../../libs/observability/src/instrumentation.ts
|
|
26
28
|
/**
|
|
27
29
|
* Register OTel auto-instrumentation for common Node.js modules.
|
|
@@ -4569,6 +4571,64 @@ _Object_({
|
|
|
4569
4571
|
additionalProperties: false
|
|
4570
4572
|
});
|
|
4571
4573
|
//#endregion
|
|
4574
|
+
//#region ../../libs/tasks/src/runtime-sessions.ts
|
|
4575
|
+
var RuntimeSessionKind = Union([
|
|
4576
|
+
Literal("root"),
|
|
4577
|
+
Literal("extend"),
|
|
4578
|
+
Literal("fork")
|
|
4579
|
+
]);
|
|
4580
|
+
var RuntimeSessionCheckpointKind = Union([Literal("attempt_final")]);
|
|
4581
|
+
_Object_({
|
|
4582
|
+
id: String$1({ format: "uuid" }),
|
|
4583
|
+
teamId: String$1({ format: "uuid" }),
|
|
4584
|
+
taskId: String$1({ format: "uuid" }),
|
|
4585
|
+
attemptN: Integer({ minimum: 1 }),
|
|
4586
|
+
sourceSlotId: Union([String$1({ format: "uuid" }), Null()]),
|
|
4587
|
+
sourceRuntimeProfileId: Union([String$1({ format: "uuid" }), Null()]),
|
|
4588
|
+
sessionKind: RuntimeSessionKind,
|
|
4589
|
+
parentSessionId: Union([String$1({ format: "uuid" }), Null()]),
|
|
4590
|
+
contentType: String$1({
|
|
4591
|
+
minLength: 1,
|
|
4592
|
+
maxLength: 200
|
|
4593
|
+
}),
|
|
4594
|
+
contentEncoding: Union([String$1({
|
|
4595
|
+
minLength: 1,
|
|
4596
|
+
maxLength: 100
|
|
4597
|
+
}), Null()]),
|
|
4598
|
+
sizeBytes: Integer({ minimum: 0 }),
|
|
4599
|
+
sha256: String$1({
|
|
4600
|
+
minLength: 64,
|
|
4601
|
+
maxLength: 64
|
|
4602
|
+
}),
|
|
4603
|
+
storageClass: String$1({
|
|
4604
|
+
minLength: 1,
|
|
4605
|
+
maxLength: 100
|
|
4606
|
+
}),
|
|
4607
|
+
checkpointKind: RuntimeSessionCheckpointKind,
|
|
4608
|
+
uploadedAt: String$1({ format: "date-time" })
|
|
4609
|
+
}, { $id: "RuntimeSession" });
|
|
4610
|
+
_Object_({
|
|
4611
|
+
sourceSlotId: Optional(String$1({ format: "uuid" })),
|
|
4612
|
+
sourceRuntimeProfileId: Optional(String$1({ format: "uuid" })),
|
|
4613
|
+
sessionKind: RuntimeSessionKind,
|
|
4614
|
+
parentSessionId: Optional(String$1({ format: "uuid" }))
|
|
4615
|
+
}, {
|
|
4616
|
+
$id: "UploadRuntimeSessionQuery",
|
|
4617
|
+
additionalProperties: false
|
|
4618
|
+
});
|
|
4619
|
+
String$1({
|
|
4620
|
+
$id: "RuntimeSessionContent",
|
|
4621
|
+
description: "Runtime session content stream.",
|
|
4622
|
+
format: "binary"
|
|
4623
|
+
});
|
|
4624
|
+
_Object_({
|
|
4625
|
+
taskId: String$1({ format: "uuid" }),
|
|
4626
|
+
attemptN: Integer({ minimum: 1 })
|
|
4627
|
+
}, {
|
|
4628
|
+
$id: "RuntimeSessionAttemptParams",
|
|
4629
|
+
additionalProperties: false
|
|
4630
|
+
});
|
|
4631
|
+
//#endregion
|
|
4572
4632
|
//#region ../../libs/tasks/src/runtime-slots.ts
|
|
4573
4633
|
var RuntimeWorkspaceKind = Union([
|
|
4574
4634
|
Literal("origin"),
|
|
@@ -5553,6 +5613,7 @@ var FreeformArtifact = _Object_({
|
|
|
5553
5613
|
});
|
|
5554
5614
|
var FreeformOutput = _Object_({
|
|
5555
5615
|
summary: String$1({ minLength: 1 }),
|
|
5616
|
+
branch: Optional(String$1({ minLength: 1 })),
|
|
5556
5617
|
artifacts: Optional(_Array_(FreeformArtifact, { maxItems: 20 })),
|
|
5557
5618
|
proposedTaskType: Optional(FreeformTaskTypeProposal),
|
|
5558
5619
|
diaryEntryIds: Optional(_Array_(String$1({ format: "uuid" }))),
|
|
@@ -5565,7 +5626,7 @@ var FreeformOutput = _Object_({
|
|
|
5565
5626
|
* Server-side preflight for `freeform` task-create. Runs after the
|
|
5566
5627
|
* sync TypeBox check passes and only kicks in when
|
|
5567
5628
|
* `input.continueFrom` is set — i.e. the proposer is asking to
|
|
5568
|
-
*
|
|
5629
|
+
* continue from a prior freeform attempt (#1287).
|
|
5569
5630
|
*
|
|
5570
5631
|
* Failure modes, in evaluation order:
|
|
5571
5632
|
* 1. `freeform.sourceTaskNotFound` — source task id does not resolve
|
|
@@ -5573,23 +5634,17 @@ var FreeformOutput = _Object_({
|
|
|
5573
5634
|
* 2. `freeform.sourceTaskTypeNotSupported` — source isn't `freeform`.
|
|
5574
5635
|
* v1 only supports freeform → freeform continuation.
|
|
5575
5636
|
* 3. `freeform.sourceAttemptNotCompleted` — named attempt is missing
|
|
5576
|
-
* or not in `completed` state;
|
|
5637
|
+
* or not in `completed` state; continuation only makes sense
|
|
5577
5638
|
* once the parent has produced a terminal output.
|
|
5578
5639
|
* 4. `freeform.executionWorkspaceNotInheritable` — caller set
|
|
5579
5640
|
* `execution.workspace` together with `continueFrom`. Workspace
|
|
5580
|
-
* mode for a continuation is
|
|
5581
|
-
* (
|
|
5582
|
-
*
|
|
5583
|
-
*
|
|
5584
|
-
*
|
|
5585
|
-
* 5. `freeform.sourceNotResumeEligible` — `daemonState` is null or
|
|
5586
|
-
* `slotResumableUntil` is null. Older completions (pre-#1287) and
|
|
5587
|
-
* daemons that opt out fall here.
|
|
5588
|
-
* 6. `freeform.sourceResumeExpired` — `slotResumableUntil` is in the
|
|
5589
|
-
* past; the warm slot's TTL has elapsed and no daemon is
|
|
5590
|
-
* guaranteed to still hold it.
|
|
5641
|
+
* mode for a continuation is derived by the daemon from parent runtime
|
|
5642
|
+
* context (local slot first, durable session + source attempt branch
|
|
5643
|
+
* second), so any caller-supplied override is silently dropped at the
|
|
5644
|
+
* daemon plan stage. Reject explicitly so misconfiguration surfaces at
|
|
5645
|
+
* create time.
|
|
5591
5646
|
*
|
|
5592
|
-
* Returns on the first failure
|
|
5647
|
+
* Returns on the first failure — the checks
|
|
5593
5648
|
* are sequential preconditions, later ones presume earlier ones hold.
|
|
5594
5649
|
*/
|
|
5595
5650
|
async function validateFreeformInputAsync(input, ctx) {
|
|
@@ -5608,7 +5663,7 @@ async function validateFreeformInputAsync(input, ctx) {
|
|
|
5608
5663
|
}];
|
|
5609
5664
|
if (input.execution?.workspace) return [{
|
|
5610
5665
|
field: "input/execution/workspace",
|
|
5611
|
-
message: "execution.workspace is
|
|
5666
|
+
message: "execution.workspace is derived from parent runtime context when continueFrom is set; omit it",
|
|
5612
5667
|
code: "freeform.executionWorkspaceNotInheritable"
|
|
5613
5668
|
}];
|
|
5614
5669
|
if (ctx.deferReadinessChecks) return [];
|
|
@@ -5618,17 +5673,6 @@ async function validateFreeformInputAsync(input, ctx) {
|
|
|
5618
5673
|
message: `Source attempt ${cf.attemptN} on task ${cf.taskId} is not in 'completed' state`,
|
|
5619
5674
|
code: "freeform.sourceAttemptNotCompleted"
|
|
5620
5675
|
}];
|
|
5621
|
-
if (!attempt.daemonState || attempt.daemonState.slotResumableUntil === null) return [{
|
|
5622
|
-
field: "input/continueFrom",
|
|
5623
|
-
message: "Source attempt did not report continuation eligibility (older completion or daemon opted out)",
|
|
5624
|
-
code: "freeform.sourceNotResumeEligible"
|
|
5625
|
-
}];
|
|
5626
|
-
const expiresAt = new Date(attempt.daemonState.slotResumableUntil).getTime();
|
|
5627
|
-
if (Number.isNaN(expiresAt) || expiresAt <= Date.now()) return [{
|
|
5628
|
-
field: "input/continueFrom",
|
|
5629
|
-
message: `Source attempt's warm slot expired at ${attempt.daemonState.slotResumableUntil} (reported at ${attempt.daemonState.reportedAt})`,
|
|
5630
|
-
code: "freeform.sourceResumeExpired"
|
|
5631
|
-
}];
|
|
5632
5676
|
return [];
|
|
5633
5677
|
}
|
|
5634
5678
|
//#endregion
|
|
@@ -8922,11 +8966,12 @@ var MAX_CLAIM_CONDITION_STATUSES = 8;
|
|
|
8922
8966
|
/**
|
|
8923
8967
|
* Daemon-asserted runtime state stamped onto a `TaskAttemptSummary` at
|
|
8924
8968
|
* attempt-completion time. The server persists this block verbatim and
|
|
8925
|
-
*
|
|
8926
|
-
* eligibility
|
|
8927
|
-
*
|
|
8928
|
-
*
|
|
8929
|
-
*
|
|
8969
|
+
* exposes `slotResumableUntil` as a legacy/local warm-slot hint; task
|
|
8970
|
+
* continuation eligibility is based on the completed source attempt and
|
|
8971
|
+
* daemon-side claim-affinity/runtime-session recovery. The block carries
|
|
8972
|
+
* its own `reportedAt` so consumers can reason about staleness without
|
|
8973
|
+
* reading documentation. All daemon-asserted state lives here —
|
|
8974
|
+
* top-level attempt fields stay server-authoritative.
|
|
8930
8975
|
*
|
|
8931
8976
|
* Adding new fields requires explicit design review (intentional
|
|
8932
8977
|
* boundary; see docs/superpowers/specs/2026-06-04-tasks-continue-design.md).
|
|
@@ -9572,12 +9617,14 @@ var ProducerContextResolutionError = class extends Error {
|
|
|
9572
9617
|
};
|
|
9573
9618
|
function createExecutionPlanCache(args) {
|
|
9574
9619
|
const cache = /* @__PURE__ */ new Map();
|
|
9620
|
+
const runtimeSessionStore = args.runtimeSessionStore ?? createNullRuntimeSessionStore();
|
|
9621
|
+
const sourceAttemptResolver = args.sourceAttemptResolver ?? createNullSourceAttemptResolver();
|
|
9575
9622
|
return {
|
|
9576
9623
|
async getOrCreate(claimedTask) {
|
|
9577
9624
|
const key = buildClaimedTaskKey(claimedTask);
|
|
9578
9625
|
const existing = cache.get(key);
|
|
9579
9626
|
if (existing) return existing;
|
|
9580
|
-
const plan = await maybeAttachWarmSlotContext(claimedTask, buildDaemonTaskExecutionPlan(claimedTask.task, args.stateDirs, args.slotIdentity, args.warmSessionTtlSec, args.workspacePolicy), args.stateDirs, args.slotRegistry);
|
|
9627
|
+
const plan = await maybeAttachWarmSlotContext(claimedTask, buildDaemonTaskExecutionPlan(claimedTask.task, args.stateDirs, args.slotIdentity, args.warmSessionTtlSec, args.workspacePolicy), args.stateDirs, args.slotRegistry, runtimeSessionStore, sourceAttemptResolver);
|
|
9581
9628
|
assertPlanAllowedByWorkspacePolicy(plan, args.workspacePolicy);
|
|
9582
9629
|
cache.set(key, plan);
|
|
9583
9630
|
return plan;
|
|
@@ -9587,6 +9634,22 @@ function createExecutionPlanCache(args) {
|
|
|
9587
9634
|
}
|
|
9588
9635
|
};
|
|
9589
9636
|
}
|
|
9637
|
+
function createNullSourceAttemptResolver() {
|
|
9638
|
+
return { findOutputBranch() {
|
|
9639
|
+
return Promise.resolve(null);
|
|
9640
|
+
} };
|
|
9641
|
+
}
|
|
9642
|
+
function createNullRuntimeSessionStore() {
|
|
9643
|
+
return {
|
|
9644
|
+
findRuntimeSessionByTaskAttempt() {
|
|
9645
|
+
return Promise.resolve(null);
|
|
9646
|
+
},
|
|
9647
|
+
hydrateSession() {
|
|
9648
|
+
return Promise.reject(new ProducerContextResolutionError("Cannot hydrate runtime session: no runtime session store configured"));
|
|
9649
|
+
},
|
|
9650
|
+
async uploadAttemptFinal() {}
|
|
9651
|
+
};
|
|
9652
|
+
}
|
|
9590
9653
|
function assertPlanAllowedByWorkspacePolicy(plan, policy) {
|
|
9591
9654
|
const allowed = new Set(policy?.allowedWorkspaceModes && policy.allowedWorkspaceModes.length > 0 ? policy.allowedWorkspaceModes : [
|
|
9592
9655
|
"none",
|
|
@@ -9604,10 +9667,26 @@ function planToRuntimeProfileWorkspaceMode(plan) {
|
|
|
9604
9667
|
function buildClaimedTaskKey(task) {
|
|
9605
9668
|
return `${task.task.id}:${task.attemptN}`;
|
|
9606
9669
|
}
|
|
9607
|
-
async function
|
|
9670
|
+
async function hydrateRemoteRuntimeSession(runtimeSessionStore, teamId, sourceTaskId, sourceAttemptN, stateDirs) {
|
|
9671
|
+
if (!await runtimeSessionStore.findRuntimeSessionByTaskAttempt(teamId, sourceTaskId, sourceAttemptN)) return null;
|
|
9672
|
+
return runtimeSessionStore.hydrateSession({
|
|
9673
|
+
attemptN: sourceAttemptN,
|
|
9674
|
+
destinationDir: `${stateDirs.piSessionsDir}/remote-${sourceTaskId}-attempt-${sourceAttemptN}`,
|
|
9675
|
+
taskId: sourceTaskId,
|
|
9676
|
+
teamId
|
|
9677
|
+
});
|
|
9678
|
+
}
|
|
9679
|
+
async function resolveWarmSlot(slotRegistry, runtimeSessionStore, teamId, sourceTaskId, sourceAttemptN, stateDirs) {
|
|
9608
9680
|
const producerContext = await slotRegistry.findLatestSlotByTaskAttempt(teamId, sourceTaskId, sourceAttemptN);
|
|
9609
|
-
if (!producerContext)
|
|
9610
|
-
|
|
9681
|
+
if (!producerContext) {
|
|
9682
|
+
const remoteSessionPath = await hydrateRemoteRuntimeSession(runtimeSessionStore, teamId, sourceTaskId, sourceAttemptN, stateDirs);
|
|
9683
|
+
return remoteSessionPath ? {
|
|
9684
|
+
kind: "remote-session",
|
|
9685
|
+
sessionPath: remoteSessionPath
|
|
9686
|
+
} : { kind: "missing" };
|
|
9687
|
+
}
|
|
9688
|
+
const localSessionPath = resolveProducerSessionPath(producerContext);
|
|
9689
|
+
const sourceSessionPath = localSessionPath ? localSessionPath : await hydrateRemoteRuntimeSession(runtimeSessionStore, teamId, sourceTaskId, sourceAttemptN, stateDirs);
|
|
9611
9690
|
if (!sourceSessionPath) return { kind: "no-session-path" };
|
|
9612
9691
|
return {
|
|
9613
9692
|
kind: "found",
|
|
@@ -9616,19 +9695,54 @@ async function resolveWarmSlot(slotRegistry, teamId, sourceTaskId, sourceAttempt
|
|
|
9616
9695
|
workspacePath: resolveProducerWorkspaceCopySource(producerContext, stateDirs)
|
|
9617
9696
|
};
|
|
9618
9697
|
}
|
|
9619
|
-
async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slotRegistry) {
|
|
9698
|
+
async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slotRegistry, runtimeSessionStore, sourceAttemptResolver) {
|
|
9620
9699
|
if (claimedTask.task.taskType === "freeform") {
|
|
9621
9700
|
const continueFrom = claimedTask.task.input.continueFrom;
|
|
9622
9701
|
if (!continueFrom) return basePlan;
|
|
9623
|
-
const resolution = await resolveWarmSlot(slotRegistry, claimedTask.task.teamId, continueFrom.taskId, continueFrom.attemptN, stateDirs);
|
|
9624
|
-
if (resolution.kind === "missing") throw new ProducerContextResolutionError(`Continuation source task ${continueFrom.taskId} attempt ${continueFrom.attemptN} has no
|
|
9702
|
+
const resolution = await resolveWarmSlot(slotRegistry, runtimeSessionStore, claimedTask.task.teamId, continueFrom.taskId, continueFrom.attemptN, stateDirs);
|
|
9703
|
+
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`);
|
|
9625
9704
|
if (resolution.kind === "no-session-path") throw new ProducerContextResolutionError(`Continuation source attempt ${continueFrom.taskId}/${continueFrom.attemptN} has no persisted Pi session path`);
|
|
9626
9705
|
const sessionDir = `${stateDirs.piSessionsDir}/continue-${claimedTask.task.id}-attempt-${claimedTask.attemptN}`;
|
|
9706
|
+
if (resolution.kind === "remote-session") {
|
|
9707
|
+
const recoveredBranch = await sourceAttemptResolver.findOutputBranch({
|
|
9708
|
+
attemptN: continueFrom.attemptN,
|
|
9709
|
+
taskId: continueFrom.taskId
|
|
9710
|
+
});
|
|
9711
|
+
if (continueFrom.mode === "fork") {
|
|
9712
|
+
if (recoveredBranch) {
|
|
9713
|
+
const forkWorkspaceId = `fork-${claimedTask.task.id}-attempt-${claimedTask.attemptN}`;
|
|
9714
|
+
const forkBranch = buildForkBranch(recoveredBranch, claimedTask.task.id, claimedTask.attemptN);
|
|
9715
|
+
return {
|
|
9716
|
+
...basePlan,
|
|
9717
|
+
workspaceMode: "dedicated_worktree",
|
|
9718
|
+
workspaceId: forkWorkspaceId,
|
|
9719
|
+
worktreeBranch: forkBranch,
|
|
9720
|
+
worktreeBaseRef: recoveredBranch,
|
|
9721
|
+
workspaceKind: "fork",
|
|
9722
|
+
sessionPersistence: {
|
|
9723
|
+
sessionDir,
|
|
9724
|
+
forkFromSessionPath: resolution.sessionPath
|
|
9725
|
+
}
|
|
9726
|
+
};
|
|
9727
|
+
}
|
|
9728
|
+
throw new ProducerContextResolutionError(`Cannot fork continuation of ${continueFrom.taskId}/${continueFrom.attemptN}: durable runtime session is available but the source attempt output did not report a branch`);
|
|
9729
|
+
}
|
|
9730
|
+
return {
|
|
9731
|
+
...basePlan,
|
|
9732
|
+
workspaceMode: "dedicated_worktree",
|
|
9733
|
+
workspaceId: recoveredBranch ? `extend-${continueFrom.taskId}-attempt-${continueFrom.attemptN}` : null,
|
|
9734
|
+
worktreeBranch: recoveredBranch,
|
|
9735
|
+
sessionPersistence: {
|
|
9736
|
+
sessionDir,
|
|
9737
|
+
forkFromSessionPath: resolution.sessionPath
|
|
9738
|
+
}
|
|
9739
|
+
};
|
|
9740
|
+
}
|
|
9627
9741
|
const parentBranch = resolution.producerSlot.workspace?.worktreeBranch ?? null;
|
|
9628
9742
|
if (continueFrom.mode === "fork") {
|
|
9629
9743
|
if (!parentBranch) throw new ProducerContextResolutionError(`Cannot fork continuation of ${continueFrom.taskId}/${continueFrom.attemptN}: producer slot has no worktree branch to fork from`);
|
|
9630
9744
|
const forkWorkspaceId = `fork-${claimedTask.task.id}-attempt-${claimedTask.attemptN}`;
|
|
9631
|
-
const forkBranch =
|
|
9745
|
+
const forkBranch = buildForkBranch(parentBranch, claimedTask.task.id, claimedTask.attemptN);
|
|
9632
9746
|
return {
|
|
9633
9747
|
...basePlan,
|
|
9634
9748
|
workspaceMode: "dedicated_worktree",
|
|
@@ -9657,9 +9771,10 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
|
|
|
9657
9771
|
const targetTaskId = typeof claimedTask.task.input.targetTaskId === "string" ? claimedTask.task.input.targetTaskId : null;
|
|
9658
9772
|
const targetAttemptN = typeof claimedTask.task.input.targetAttemptN === "number" ? claimedTask.task.input.targetAttemptN : null;
|
|
9659
9773
|
if (!targetTaskId || !targetAttemptN) throw new ProducerContextResolutionError("judge_eval_attempt is missing targetTaskId/targetAttemptN");
|
|
9660
|
-
const resolution = await resolveWarmSlot(slotRegistry, claimedTask.task.teamId, targetTaskId, targetAttemptN, stateDirs);
|
|
9774
|
+
const resolution = await resolveWarmSlot(slotRegistry, runtimeSessionStore, claimedTask.task.teamId, targetTaskId, targetAttemptN, stateDirs);
|
|
9661
9775
|
if (resolution.kind === "missing") throw new ProducerContextResolutionError(`No live producer runtime slot found for task ${targetTaskId} attempt ${targetAttemptN}`);
|
|
9662
9776
|
if (resolution.kind === "no-session-path") throw new ProducerContextResolutionError(`Producer task ${targetTaskId} attempt ${targetAttemptN} has no persisted Pi session path`);
|
|
9777
|
+
if (resolution.kind === "remote-session") throw new ProducerContextResolutionError(`Producer task ${targetTaskId} attempt ${targetAttemptN} has a durable runtime session but no workspace metadata to copy`);
|
|
9663
9778
|
return {
|
|
9664
9779
|
...basePlan,
|
|
9665
9780
|
workspaceMode: "scratch_mount",
|
|
@@ -9675,6 +9790,9 @@ async function maybeAttachWarmSlotContext(claimedTask, basePlan, stateDirs, slot
|
|
|
9675
9790
|
}
|
|
9676
9791
|
};
|
|
9677
9792
|
}
|
|
9793
|
+
function buildForkBranch(parentBranch, childTaskId, childAttemptN) {
|
|
9794
|
+
return `${parentBranch}-fork-${childTaskId.slice(0, 8)}-${childAttemptN}`;
|
|
9795
|
+
}
|
|
9678
9796
|
function resolveProducerSessionPath(producer) {
|
|
9679
9797
|
const explicit = producer.session?.sessionPath ?? null;
|
|
9680
9798
|
if (explicit && existsSync(explicit)) return explicit;
|
|
@@ -9704,12 +9822,11 @@ function recoverScratchWorkspacePath(producer, stateDirs) {
|
|
|
9704
9822
|
//#endregion
|
|
9705
9823
|
//#region src/lib/finalize.ts
|
|
9706
9824
|
/**
|
|
9707
|
-
* Build the `daemonState` payload for a `/complete` call.
|
|
9708
|
-
* attempts
|
|
9709
|
-
*
|
|
9710
|
-
*
|
|
9711
|
-
*
|
|
9712
|
-
* fail validation with `freeform.sourceNotResumeEligible`.
|
|
9825
|
+
* Build the `daemonState` payload for a `/complete` call. Freeform
|
|
9826
|
+
* attempts report the local warm-slot hint when one exists; slot-less
|
|
9827
|
+
* freeform completions report `null` for `slotResumableUntil`. The server
|
|
9828
|
+
* persists this verbatim as diagnostic/runtime metadata, not as the
|
|
9829
|
+
* continuation eligibility gate.
|
|
9713
9830
|
*
|
|
9714
9831
|
* Returns `null` for non-freeform task types so the field is omitted
|
|
9715
9832
|
* from the request body (the server treats null and absent the same).
|
|
@@ -10067,6 +10184,70 @@ function isExecutable(path) {
|
|
|
10067
10184
|
}
|
|
10068
10185
|
}
|
|
10069
10186
|
//#endregion
|
|
10187
|
+
//#region src/lib/runtime-sessions.ts
|
|
10188
|
+
function resolveRuntimeSessionKind(claimedTask) {
|
|
10189
|
+
const continueFrom = resolveContinueFrom(claimedTask);
|
|
10190
|
+
if (!continueFrom) return "root";
|
|
10191
|
+
return continueFrom.mode === "fork" ? "fork" : "extend";
|
|
10192
|
+
}
|
|
10193
|
+
async function resolveParentRuntimeSession(runtimeSessionStore, claimedTask) {
|
|
10194
|
+
const continueFrom = resolveContinueFrom(claimedTask);
|
|
10195
|
+
if (!continueFrom) return null;
|
|
10196
|
+
return runtimeSessionStore.findRuntimeSessionByTaskAttempt(claimedTask.task.teamId, continueFrom.taskId, continueFrom.attemptN);
|
|
10197
|
+
}
|
|
10198
|
+
function applyRuntimeSessionUploadFailure(output, err) {
|
|
10199
|
+
if (output.status !== "completed") return output;
|
|
10200
|
+
return {
|
|
10201
|
+
...output,
|
|
10202
|
+
contentSignature: void 0,
|
|
10203
|
+
error: {
|
|
10204
|
+
code: "runtime_session_upload_failed",
|
|
10205
|
+
message: "Task completed, but durable runtime session checkpoint upload failed: " + (err instanceof Error ? err.message : String(err)),
|
|
10206
|
+
retryable: true
|
|
10207
|
+
},
|
|
10208
|
+
output: null,
|
|
10209
|
+
outputCid: null,
|
|
10210
|
+
status: "failed"
|
|
10211
|
+
};
|
|
10212
|
+
}
|
|
10213
|
+
function createApiRuntimeSessionStore(args) {
|
|
10214
|
+
const { agent } = args;
|
|
10215
|
+
return {
|
|
10216
|
+
async findRuntimeSessionByTaskAttempt(teamId, taskId, attemptN) {
|
|
10217
|
+
return agent.runtimeSessions.getForAttempt({
|
|
10218
|
+
attemptN,
|
|
10219
|
+
taskId
|
|
10220
|
+
}, { teamId });
|
|
10221
|
+
},
|
|
10222
|
+
async hydrateSession(input) {
|
|
10223
|
+
const downloaded = await agent.runtimeSessions.download({
|
|
10224
|
+
attemptN: input.attemptN,
|
|
10225
|
+
taskId: input.taskId
|
|
10226
|
+
}, { teamId: input.teamId });
|
|
10227
|
+
await mkdir(input.destinationDir, { recursive: true });
|
|
10228
|
+
const sessionPath = join(input.destinationDir, `remote-${input.taskId}-attempt-${input.attemptN}.jsonl`);
|
|
10229
|
+
await pipeline(downloaded, createWriteStream(sessionPath));
|
|
10230
|
+
return sessionPath;
|
|
10231
|
+
},
|
|
10232
|
+
async uploadAttemptFinal(input) {
|
|
10233
|
+
const sessionPath = resolveLatestPiSessionPath(input.sessionDir);
|
|
10234
|
+
if (!sessionPath) throw new Error(`Cannot upload runtime session for ${input.taskId}/${input.attemptN}: no local session file in ${input.sessionDir}`);
|
|
10235
|
+
await agent.runtimeSessions.upload({
|
|
10236
|
+
attemptN: input.attemptN,
|
|
10237
|
+
taskId: input.taskId
|
|
10238
|
+
}, createReadStream(sessionPath), {
|
|
10239
|
+
parentSessionId: input.parentSessionId ?? void 0,
|
|
10240
|
+
sessionKind: input.sessionKind,
|
|
10241
|
+
sourceRuntimeProfileId: input.sourceRuntimeProfileId ?? void 0,
|
|
10242
|
+
sourceSlotId: input.sourceSlotId ?? void 0
|
|
10243
|
+
}, { teamId: input.teamId });
|
|
10244
|
+
}
|
|
10245
|
+
};
|
|
10246
|
+
}
|
|
10247
|
+
function resolveContinueFrom(claimedTask) {
|
|
10248
|
+
return claimedTask.task.input.continueFrom;
|
|
10249
|
+
}
|
|
10250
|
+
//#endregion
|
|
10070
10251
|
//#region src/lib/runtime-slots.ts
|
|
10071
10252
|
function createApiRuntimeSlotStore(args) {
|
|
10072
10253
|
const { agent } = args;
|
|
@@ -10108,7 +10289,11 @@ function createApiRuntimeSlotStore(args) {
|
|
|
10108
10289
|
}, { teamId });
|
|
10109
10290
|
if (!resolved) return null;
|
|
10110
10291
|
return {
|
|
10111
|
-
slot: {
|
|
10292
|
+
slot: {
|
|
10293
|
+
expiresAtMs: resolved.slot.expiresAtMs,
|
|
10294
|
+
id: resolved.slot.id,
|
|
10295
|
+
runtimeProfileId: resolved.slot.runtimeProfileId
|
|
10296
|
+
},
|
|
10112
10297
|
session: resolved.slot.sessionDir ? {
|
|
10113
10298
|
sessionDir: resolved.slot.sessionDir,
|
|
10114
10299
|
sessionPath: resolved.slot.sessionPath
|
|
@@ -10157,6 +10342,21 @@ function signalExitCode(signal) {
|
|
|
10157
10342
|
return signal === "SIGINT" ? 130 : 143;
|
|
10158
10343
|
}
|
|
10159
10344
|
//#endregion
|
|
10345
|
+
//#region src/lib/source-attempts.ts
|
|
10346
|
+
function createApiSourceAttemptResolver(args) {
|
|
10347
|
+
const { agent } = args;
|
|
10348
|
+
return { async findOutputBranch(input) {
|
|
10349
|
+
const attempt = (await agent.tasks.listAttempts(input.taskId)).find((candidate) => candidate.attemptN === input.attemptN);
|
|
10350
|
+
if (!attempt || attempt.status !== "completed") return null;
|
|
10351
|
+
return resolveOutputBranch(attempt.output);
|
|
10352
|
+
} };
|
|
10353
|
+
}
|
|
10354
|
+
function resolveOutputBranch(output) {
|
|
10355
|
+
if (!output || typeof output !== "object") return null;
|
|
10356
|
+
const branch = output.branch;
|
|
10357
|
+
return typeof branch === "string" && branch.length > 0 ? branch : null;
|
|
10358
|
+
}
|
|
10359
|
+
//#endregion
|
|
10160
10360
|
//#region src/lib/state-dir.ts
|
|
10161
10361
|
function ensureDaemonStateDirs(mountPath) {
|
|
10162
10362
|
const rootDir = join(mountPath, ".moltnet", "d");
|
|
@@ -10263,6 +10463,8 @@ async function runPolling(opts) {
|
|
|
10263
10463
|
});
|
|
10264
10464
|
for (const profile of profiles) validateRuntimeProfilePrerequisites(profile, cfg.profilePrerequisiteEnv, cfg.profilePrerequisitePath);
|
|
10265
10465
|
const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
|
|
10466
|
+
const runtimeSessionStore = createApiRuntimeSessionStore({ agent: ctx.agent });
|
|
10467
|
+
const sourceAttemptResolver = createApiSourceAttemptResolver({ agent: ctx.agent });
|
|
10266
10468
|
const runtimes = /* @__PURE__ */ new Map();
|
|
10267
10469
|
for (const profile of profiles) {
|
|
10268
10470
|
const common = parseCommonOptions(values, { runtimeDefaults: {
|
|
@@ -10292,7 +10494,9 @@ async function runPolling(opts) {
|
|
|
10292
10494
|
defaultWorkspaceMode: profile.defaultWorkspaceMode,
|
|
10293
10495
|
allowedWorkspaceModes: profile.allowedWorkspaceModes
|
|
10294
10496
|
},
|
|
10295
|
-
slotRegistry
|
|
10497
|
+
slotRegistry,
|
|
10498
|
+
runtimeSessionStore,
|
|
10499
|
+
sourceAttemptResolver
|
|
10296
10500
|
});
|
|
10297
10501
|
runtimes.set(profile.id, {
|
|
10298
10502
|
common,
|
|
@@ -10401,7 +10605,9 @@ async function runPolling(opts) {
|
|
|
10401
10605
|
stopWhenEmpty: opts.stopWhenEmpty,
|
|
10402
10606
|
debug: baseCommon.debug,
|
|
10403
10607
|
logger: rootLogger,
|
|
10404
|
-
slotRegistry
|
|
10608
|
+
slotRegistry,
|
|
10609
|
+
sessionRegistry: runtimeSessionStore,
|
|
10610
|
+
sourceAttemptResolver
|
|
10405
10611
|
}),
|
|
10406
10612
|
makeReporter: (claimedTask) => {
|
|
10407
10613
|
const selected = runtimeForClaimedTask(runtimes, claimedTask);
|
|
@@ -10416,7 +10622,28 @@ async function runPolling(opts) {
|
|
|
10416
10622
|
onTaskFinished: async (output, claimedTask) => {
|
|
10417
10623
|
const selected = runtimeForClaimedTask(runtimes, claimedTask);
|
|
10418
10624
|
const resolved = await slotRegistry.findLatestSlotByTaskAttempt(claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN);
|
|
10419
|
-
|
|
10625
|
+
let terminalOutput = output;
|
|
10626
|
+
if (resolved?.session?.sessionDir) try {
|
|
10627
|
+
const parentSession = await resolveParentRuntimeSession(runtimeSessionStore, claimedTask);
|
|
10628
|
+
await runtimeSessionStore.uploadAttemptFinal({
|
|
10629
|
+
attemptN: claimedTask.attemptN,
|
|
10630
|
+
parentSessionId: parentSession?.id ?? null,
|
|
10631
|
+
sessionDir: resolved.session.sessionDir,
|
|
10632
|
+
sessionKind: resolveRuntimeSessionKind(claimedTask),
|
|
10633
|
+
sourceRuntimeProfileId: resolved.slot.runtimeProfileId,
|
|
10634
|
+
sourceSlotId: resolved.slot.id,
|
|
10635
|
+
taskId: claimedTask.task.id,
|
|
10636
|
+
teamId: claimedTask.task.teamId
|
|
10637
|
+
});
|
|
10638
|
+
} catch (err) {
|
|
10639
|
+
rootLogger.error({
|
|
10640
|
+
err,
|
|
10641
|
+
taskId: claimedTask.task.id,
|
|
10642
|
+
attemptN: claimedTask.attemptN
|
|
10643
|
+
}, "agent-daemon.runtime_session_upload_failed");
|
|
10644
|
+
terminalOutput = applyRuntimeSessionUploadFailure(output, err);
|
|
10645
|
+
}
|
|
10646
|
+
return finalizeTask(ctx.agent, terminalOutput, {
|
|
10420
10647
|
task: claimedTask.task,
|
|
10421
10648
|
slot: resolved ? { expiresAtMs: resolved.slot.expiresAtMs } : null,
|
|
10422
10649
|
writeCorrelationAnchors: makePrBodyAnchorWriter({
|
|
@@ -10681,6 +10908,8 @@ async function runOnce(argv) {
|
|
|
10681
10908
|
activatePiCodingAgentDir(piAgentDir.path);
|
|
10682
10909
|
const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
|
|
10683
10910
|
const slotRegistry = createApiRuntimeSlotStore({ agent: ctx.agent });
|
|
10911
|
+
const runtimeSessionStore = createApiRuntimeSessionStore({ agent: ctx.agent });
|
|
10912
|
+
const sourceAttemptResolver = createApiSourceAttemptResolver({ agent: ctx.agent });
|
|
10684
10913
|
const slotIdentity = {
|
|
10685
10914
|
agentName: opts.agent,
|
|
10686
10915
|
runtimeProfileId: profile.id
|
|
@@ -10693,7 +10922,9 @@ async function runOnce(argv) {
|
|
|
10693
10922
|
defaultWorkspaceMode: profile.defaultWorkspaceMode,
|
|
10694
10923
|
allowedWorkspaceModes: profile.allowedWorkspaceModes
|
|
10695
10924
|
},
|
|
10696
|
-
slotRegistry
|
|
10925
|
+
slotRegistry,
|
|
10926
|
+
runtimeSessionStore,
|
|
10927
|
+
sourceAttemptResolver
|
|
10697
10928
|
});
|
|
10698
10929
|
const otelShutdown = await initWorkerOtel({
|
|
10699
10930
|
serviceName: "moltnet.agent-daemon.once",
|
|
@@ -10853,7 +11084,28 @@ async function runOnce(argv) {
|
|
|
10853
11084
|
}),
|
|
10854
11085
|
onTaskFinished: async (output, claimedTask) => {
|
|
10855
11086
|
const resolved = await slotRegistry.findLatestSlotByTaskAttempt(claimedTask.task.teamId, claimedTask.task.id, claimedTask.attemptN);
|
|
10856
|
-
|
|
11087
|
+
let terminalOutput = output;
|
|
11088
|
+
if (resolved?.session?.sessionDir) try {
|
|
11089
|
+
const parentSession = await resolveParentRuntimeSession(runtimeSessionStore, claimedTask);
|
|
11090
|
+
await runtimeSessionStore.uploadAttemptFinal({
|
|
11091
|
+
attemptN: claimedTask.attemptN,
|
|
11092
|
+
parentSessionId: parentSession?.id ?? null,
|
|
11093
|
+
sessionDir: resolved.session.sessionDir,
|
|
11094
|
+
sessionKind: resolveRuntimeSessionKind(claimedTask),
|
|
11095
|
+
sourceRuntimeProfileId: resolved.slot.runtimeProfileId,
|
|
11096
|
+
sourceSlotId: resolved.slot.id,
|
|
11097
|
+
taskId: claimedTask.task.id,
|
|
11098
|
+
teamId: claimedTask.task.teamId
|
|
11099
|
+
});
|
|
11100
|
+
} catch (err) {
|
|
11101
|
+
rootLogger.error({
|
|
11102
|
+
err,
|
|
11103
|
+
taskId: claimedTask.task.id,
|
|
11104
|
+
attemptN: claimedTask.attemptN
|
|
11105
|
+
}, "agent-daemon.runtime_session_upload_failed");
|
|
11106
|
+
terminalOutput = applyRuntimeSessionUploadFailure(output, err);
|
|
11107
|
+
}
|
|
11108
|
+
return finalizeTask(ctx.agent, terminalOutput, {
|
|
10857
11109
|
task: claimedTask.task,
|
|
10858
11110
|
slot: resolved ? { expiresAtMs: resolved.slot.expiresAtMs } : null,
|
|
10859
11111
|
writeCorrelationAnchors,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/agent-daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "MoltNet agent daemon — claims and executes tasks (fulfill_brief, assess_brief) from the MoltNet task-service via Pi-headless. CLI: moltnet-agent.",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"@opentelemetry/semantic-conventions": "^1.39.0",
|
|
46
46
|
"pino": "^10.3.1",
|
|
47
47
|
"pino-pretty": "^13.1.3",
|
|
48
|
-
"@themoltnet/
|
|
49
|
-
"@themoltnet/
|
|
50
|
-
"@themoltnet/
|
|
48
|
+
"@themoltnet/pi-extension": "0.27.3",
|
|
49
|
+
"@themoltnet/sdk": "0.113.1",
|
|
50
|
+
"@themoltnet/agent-runtime": "0.31.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"tsx": "^4.7.0",
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
"vite": "^8.0.0",
|
|
56
56
|
"vitest": "^3.0.0",
|
|
57
57
|
"@moltnet/observability": "0.1.0",
|
|
58
|
-
"@moltnet/
|
|
58
|
+
"@moltnet/tasks": "0.1.0",
|
|
59
59
|
"@moltnet/crypto-service": "0.1.0",
|
|
60
|
-
"@moltnet/
|
|
60
|
+
"@moltnet/bootstrap": "0.1.0"
|
|
61
61
|
},
|
|
62
62
|
"nx": {
|
|
63
63
|
"tags": [
|