@themoltnet/agent-daemon 0.6.2 → 0.7.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/dist/main.js +715 -110
- package/package.json +5 -5
package/dist/main.js
CHANGED
|
@@ -12,6 +12,7 @@ import { execFile, execFileSync } from "node:child_process";
|
|
|
12
12
|
import { DefaultResourceLoader, SessionManager, createAgentSession, createBashToolDefinition, createEditToolDefinition, createReadToolDefinition, createSyntheticSourceInfo, createWriteToolDefinition, defineTool, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { Type, getModel } from "@earendil-works/pi-ai";
|
|
14
14
|
import { MemoryProvider, RealFSProvider, ShadowProvider, VM, VmCheckpoint, createHttpHooks, createShadowPathPredicate, ensureImageSelector, loadGuestAssets } from "@earendil-works/gondolin";
|
|
15
|
+
import { DatabaseSync } from "node:sqlite";
|
|
15
16
|
import { once } from "node:events";
|
|
16
17
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
17
18
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
@@ -4756,7 +4757,10 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
4756
4757
|
inputSchema: FulfillBriefInput,
|
|
4757
4758
|
outputSchema: FulfillBriefOutput,
|
|
4758
4759
|
outputKind: "artifact",
|
|
4760
|
+
resumable: true,
|
|
4759
4761
|
workspaceMode: "dedicated_worktree",
|
|
4762
|
+
workspaceScope: "session",
|
|
4763
|
+
sessionScope: "correlation",
|
|
4760
4764
|
requiresReferences: false,
|
|
4761
4765
|
validateOutput: requireVerificationWhenCriteriaPresent
|
|
4762
4766
|
},
|
|
@@ -4766,6 +4770,8 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
4766
4770
|
outputSchema: AssessBriefOutput,
|
|
4767
4771
|
outputKind: "judgment",
|
|
4768
4772
|
workspaceMode: "dedicated_worktree",
|
|
4773
|
+
workspaceScope: "attempt",
|
|
4774
|
+
sessionScope: "none",
|
|
4769
4775
|
requiresReferences: true,
|
|
4770
4776
|
validateInput: validateJudgmentInput,
|
|
4771
4777
|
validateInputAsync: validateAssessBriefInputAsync
|
|
@@ -4775,6 +4781,8 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
4775
4781
|
inputSchema: CuratePackInput,
|
|
4776
4782
|
outputSchema: CuratePackOutput,
|
|
4777
4783
|
outputKind: "artifact",
|
|
4784
|
+
workspaceScope: "attempt",
|
|
4785
|
+
sessionScope: "none",
|
|
4778
4786
|
requiresReferences: false,
|
|
4779
4787
|
validateOutput: requireVerificationWhenCriteriaPresent
|
|
4780
4788
|
},
|
|
@@ -4783,6 +4791,8 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
4783
4791
|
inputSchema: RenderPackInput,
|
|
4784
4792
|
outputSchema: RenderPackOutput,
|
|
4785
4793
|
outputKind: "artifact",
|
|
4794
|
+
workspaceScope: "attempt",
|
|
4795
|
+
sessionScope: "none",
|
|
4786
4796
|
requiresReferences: false,
|
|
4787
4797
|
validateOutput: requireVerificationWhenCriteriaPresent,
|
|
4788
4798
|
validateInputAsync: validateRenderPackInputAsync
|
|
@@ -4792,6 +4802,8 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
4792
4802
|
inputSchema: JudgePackInput,
|
|
4793
4803
|
outputSchema: JudgePackOutput,
|
|
4794
4804
|
outputKind: "judgment",
|
|
4805
|
+
workspaceScope: "attempt",
|
|
4806
|
+
sessionScope: "none",
|
|
4795
4807
|
requiresReferences: true,
|
|
4796
4808
|
validateInput: validateJudgmentInput,
|
|
4797
4809
|
validateOutput: validateJudgePackOutput,
|
|
@@ -4802,6 +4814,8 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
4802
4814
|
inputSchema: RunEvalInput,
|
|
4803
4815
|
outputSchema: RunEvalOutput,
|
|
4804
4816
|
outputKind: "artifact",
|
|
4817
|
+
workspaceScope: "attempt",
|
|
4818
|
+
sessionScope: "custom",
|
|
4805
4819
|
requiresReferences: false,
|
|
4806
4820
|
validateOutput: validateRunEvalOutput
|
|
4807
4821
|
},
|
|
@@ -4810,6 +4824,8 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
4810
4824
|
inputSchema: JudgeEvalVariantInput,
|
|
4811
4825
|
outputSchema: JudgeEvalVariantOutput,
|
|
4812
4826
|
outputKind: "judgment",
|
|
4827
|
+
workspaceScope: "attempt",
|
|
4828
|
+
sessionScope: "custom",
|
|
4813
4829
|
requiresReferences: false,
|
|
4814
4830
|
validateInput: validateJudgeEvalVariantInput,
|
|
4815
4831
|
validateOutput: validateJudgeEvalVariantOutput,
|
|
@@ -5732,6 +5748,49 @@ function taskTypeUsesSubagents(taskType) {
|
|
|
5732
5748
|
function taskTypeWorkspaceMode(taskType) {
|
|
5733
5749
|
return getTaskTypeEntry(taskType)?.workspaceMode ?? "shared_mount";
|
|
5734
5750
|
}
|
|
5751
|
+
/**
|
|
5752
|
+
* Whether the daemon may keep a warm executor session alive after one
|
|
5753
|
+
* attempt completes and reuse it for later related tasks.
|
|
5754
|
+
*
|
|
5755
|
+
* Unknown task types default to `false`.
|
|
5756
|
+
*/
|
|
5757
|
+
function taskTypeResumable(taskType) {
|
|
5758
|
+
return getTaskTypeEntry(taskType)?.resumable === true;
|
|
5759
|
+
}
|
|
5760
|
+
/**
|
|
5761
|
+
* Lifetime of the task type's workspace.
|
|
5762
|
+
*
|
|
5763
|
+
* Unknown task types and task types without an explicit policy default to the
|
|
5764
|
+
* disposable per-attempt behavior.
|
|
5765
|
+
*/
|
|
5766
|
+
function taskTypeWorkspaceScope(taskType) {
|
|
5767
|
+
return getTaskTypeEntry(taskType)?.workspaceScope ?? "attempt";
|
|
5768
|
+
}
|
|
5769
|
+
/**
|
|
5770
|
+
* Granularity of daemon-local warm-session reuse.
|
|
5771
|
+
*
|
|
5772
|
+
* Unknown task types and task types without an explicit policy default to no
|
|
5773
|
+
* reuse.
|
|
5774
|
+
*/
|
|
5775
|
+
function taskTypeSessionScope(taskType) {
|
|
5776
|
+
return getTaskTypeEntry(taskType)?.sessionScope ?? "none";
|
|
5777
|
+
}
|
|
5778
|
+
/**
|
|
5779
|
+
* Resolve the daemon-relevant execution policy declared by a task type.
|
|
5780
|
+
*
|
|
5781
|
+
* This is the single read path for "may this task reuse a warm session?" and
|
|
5782
|
+
* "does its workspace belong to the attempt or the session?" so daemon code
|
|
5783
|
+
* doesn't need to hand-roll defaults or peek at the raw registry shape.
|
|
5784
|
+
*/
|
|
5785
|
+
function getTaskExecutionPolicy(taskType) {
|
|
5786
|
+
return {
|
|
5787
|
+
resumable: taskTypeResumable(taskType),
|
|
5788
|
+
workspaceMode: taskTypeWorkspaceMode(taskType),
|
|
5789
|
+
workspaceScope: taskTypeWorkspaceScope(taskType),
|
|
5790
|
+
sessionScope: taskTypeSessionScope(taskType),
|
|
5791
|
+
usesSubagents: taskTypeUsesSubagents(taskType)
|
|
5792
|
+
};
|
|
5793
|
+
}
|
|
5735
5794
|
//#endregion
|
|
5736
5795
|
//#region ../../libs/tasks/src/wire.ts
|
|
5737
5796
|
/**
|
|
@@ -5994,6 +6053,10 @@ var COMMON_OPTIONAL_FLAGS = `\
|
|
|
5994
6053
|
this, the pi session aborts and the attempt
|
|
5995
6054
|
fails with code 'max_bash_timeouts_exceeded'.
|
|
5996
6055
|
0 = disabled. Default: 3.
|
|
6056
|
+
--warm-session-ttl-sec <n> Retain resumable daemon slots (Pi sessions +
|
|
6057
|
+
reusable worktrees) in local daemon state for
|
|
6058
|
+
this many seconds after use. 0 = disable reuse.
|
|
6059
|
+
Default: 1800.
|
|
5997
6060
|
--debug Verbose logging: also log successful list/claim
|
|
5998
6061
|
outcomes (candidate counts, claim attempts).`;
|
|
5999
6062
|
var REGISTERED_TASK_TYPES = Object.keys(BUILT_IN_TASK_TYPES).sort();
|
|
@@ -15329,9 +15392,11 @@ var NO_SKILLS = () => ({
|
|
|
15329
15392
|
diagnostics: []
|
|
15330
15393
|
});
|
|
15331
15394
|
/**
|
|
15332
|
-
* Construct an
|
|
15333
|
-
*
|
|
15334
|
-
*
|
|
15395
|
+
* Construct an `AgentSession`. By default it is in-memory; callers may opt
|
|
15396
|
+
* parent sessions into daemon-owned file persistence via `sessionPersistence`.
|
|
15397
|
+
* The caller is responsible for eventually invoking `session.prompt(...)` and
|
|
15398
|
+
* for tearing down — the helper does no lifecycle management beyond
|
|
15399
|
+
* construction.
|
|
15335
15400
|
*/
|
|
15336
15401
|
async function buildAgentSession(args) {
|
|
15337
15402
|
const piOtelExtension = createPiOtelExtension({
|
|
@@ -15346,15 +15411,23 @@ async function buildAgentSession(args) {
|
|
|
15346
15411
|
skillsOverride: args.skillsOverride ?? NO_SKILLS
|
|
15347
15412
|
});
|
|
15348
15413
|
await resourceLoader.reload();
|
|
15414
|
+
const sessionManager = args.sessionPersistence ? await resolvePersistentSessionManager({
|
|
15415
|
+
cwd: args.mountPath,
|
|
15416
|
+
sessionDir: args.sessionPersistence.sessionDir
|
|
15417
|
+
}) : SessionManager.inMemory(args.mountPath);
|
|
15349
15418
|
return (await createAgentSession({
|
|
15350
15419
|
agentDir: args.piAuthDir,
|
|
15351
15420
|
cwd: args.mountPath,
|
|
15352
15421
|
model: args.modelHandle,
|
|
15353
15422
|
customTools: args.customTools,
|
|
15354
|
-
sessionManager
|
|
15423
|
+
sessionManager,
|
|
15355
15424
|
resourceLoader
|
|
15356
15425
|
})).session;
|
|
15357
15426
|
}
|
|
15427
|
+
async function resolvePersistentSessionManager(args) {
|
|
15428
|
+
await SessionManager.list(args.cwd, args.sessionDir);
|
|
15429
|
+
return SessionManager.continueRecent(args.cwd, args.sessionDir);
|
|
15430
|
+
}
|
|
15358
15431
|
//#endregion
|
|
15359
15432
|
//#region ../../libs/pi-extension/src/runtime/inject-task-context.ts
|
|
15360
15433
|
/**
|
|
@@ -15965,6 +16038,113 @@ function resolveSubmitTools(taskType, opts = {}) {
|
|
|
15965
16038
|
};
|
|
15966
16039
|
}
|
|
15967
16040
|
//#endregion
|
|
16041
|
+
//#region ../../libs/pi-extension/src/runtime/task-workspace.ts
|
|
16042
|
+
function prepareTaskWorkspace(task, requestedMountPath, executionPlan) {
|
|
16043
|
+
const branch = executionPlan?.worktreeBranch ?? null;
|
|
16044
|
+
if (!branch) return {
|
|
16045
|
+
mountPath: requestedMountPath,
|
|
16046
|
+
mode: "shared_mount",
|
|
16047
|
+
branch: null,
|
|
16048
|
+
cleanup: () => {}
|
|
16049
|
+
};
|
|
16050
|
+
const mainRepo = findMainWorktree();
|
|
16051
|
+
const worktreeDir = resolveTaskWorktreePath(mainRepo, executionPlan?.workspaceId ?? `task-${task.id}`);
|
|
16052
|
+
const relMount = relative(mainRepo, requestedMountPath);
|
|
16053
|
+
const mountPath = relMount === "" || relMount.startsWith("..") ? worktreeDir : join(worktreeDir, relMount);
|
|
16054
|
+
const keepWorkspace = executionPlan?.workspaceScope === "session" && executionPlan.sessionKey !== null;
|
|
16055
|
+
if (keepWorkspace) ensureReusableTaskWorktree(mainRepo, worktreeDir, branch);
|
|
16056
|
+
else {
|
|
16057
|
+
removeExistingTaskWorktree(mainRepo, worktreeDir);
|
|
16058
|
+
addTaskWorktree(mainRepo, worktreeDir, branch);
|
|
16059
|
+
}
|
|
16060
|
+
return {
|
|
16061
|
+
mountPath,
|
|
16062
|
+
mode: "dedicated_worktree",
|
|
16063
|
+
branch,
|
|
16064
|
+
cleanup: keepWorkspace ? () => {} : () => {
|
|
16065
|
+
execFileSync("git", [
|
|
16066
|
+
"-C",
|
|
16067
|
+
mainRepo,
|
|
16068
|
+
"worktree",
|
|
16069
|
+
"remove",
|
|
16070
|
+
"--force",
|
|
16071
|
+
worktreeDir
|
|
16072
|
+
], { stdio: "pipe" });
|
|
16073
|
+
}
|
|
16074
|
+
};
|
|
16075
|
+
}
|
|
16076
|
+
function resolveTaskWorktreePath(mainRepo, workspaceId) {
|
|
16077
|
+
return join(mainRepo, ".worktrees", workspaceId);
|
|
16078
|
+
}
|
|
16079
|
+
function ensureReusableTaskWorktree(mainRepo, worktreeDir, branch) {
|
|
16080
|
+
if (isRegisteredWorktree$1(mainRepo, worktreeDir)) return;
|
|
16081
|
+
if (existsSync(worktreeDir)) throw new Error(`Expected reusable worktree ${worktreeDir} to be git-managed, but it exists outside git worktree metadata.`);
|
|
16082
|
+
addTaskWorktree(mainRepo, worktreeDir, branch);
|
|
16083
|
+
}
|
|
16084
|
+
function addTaskWorktree(mainRepo, worktreeDir, branch) {
|
|
16085
|
+
const baseRef = resolveWorktreeBaseRef(mainRepo);
|
|
16086
|
+
execFileSync("git", gitRefExists(mainRepo, `refs/heads/${branch}`) ? [
|
|
16087
|
+
"-C",
|
|
16088
|
+
mainRepo,
|
|
16089
|
+
"worktree",
|
|
16090
|
+
"add",
|
|
16091
|
+
worktreeDir,
|
|
16092
|
+
branch
|
|
16093
|
+
] : [
|
|
16094
|
+
"-C",
|
|
16095
|
+
mainRepo,
|
|
16096
|
+
"worktree",
|
|
16097
|
+
"add",
|
|
16098
|
+
"-b",
|
|
16099
|
+
branch,
|
|
16100
|
+
worktreeDir,
|
|
16101
|
+
baseRef
|
|
16102
|
+
], { stdio: "pipe" });
|
|
16103
|
+
}
|
|
16104
|
+
function removeExistingTaskWorktree(mainRepo, worktreeDir) {
|
|
16105
|
+
if (!existsSync(worktreeDir) || !isRegisteredWorktree$1(mainRepo, worktreeDir)) return;
|
|
16106
|
+
execFileSync("git", [
|
|
16107
|
+
"-C",
|
|
16108
|
+
mainRepo,
|
|
16109
|
+
"worktree",
|
|
16110
|
+
"remove",
|
|
16111
|
+
"--force",
|
|
16112
|
+
worktreeDir
|
|
16113
|
+
], { stdio: "pipe" });
|
|
16114
|
+
}
|
|
16115
|
+
function isRegisteredWorktree$1(mainRepo, worktreeDir) {
|
|
16116
|
+
const list = execFileSync("git", [
|
|
16117
|
+
"-C",
|
|
16118
|
+
mainRepo,
|
|
16119
|
+
"worktree",
|
|
16120
|
+
"list",
|
|
16121
|
+
"--porcelain"
|
|
16122
|
+
], {
|
|
16123
|
+
encoding: "utf8",
|
|
16124
|
+
stdio: "pipe"
|
|
16125
|
+
});
|
|
16126
|
+
const marker = `worktree ${worktreeDir}\n`;
|
|
16127
|
+
return list.includes(marker) || list.endsWith(`worktree ${worktreeDir}`);
|
|
16128
|
+
}
|
|
16129
|
+
function resolveWorktreeBaseRef(mainRepo) {
|
|
16130
|
+
return gitRefExists(mainRepo, "refs/heads/main") ? "main" : "HEAD";
|
|
16131
|
+
}
|
|
16132
|
+
function gitRefExists(mainRepo, ref) {
|
|
16133
|
+
try {
|
|
16134
|
+
execFileSync("git", [
|
|
16135
|
+
"-C",
|
|
16136
|
+
mainRepo,
|
|
16137
|
+
"show-ref",
|
|
16138
|
+
"--verify",
|
|
16139
|
+
"--quiet",
|
|
16140
|
+
ref
|
|
16141
|
+
], { stdio: "pipe" });
|
|
16142
|
+
return true;
|
|
16143
|
+
} catch {
|
|
16144
|
+
return false;
|
|
16145
|
+
}
|
|
16146
|
+
}
|
|
16147
|
+
//#endregion
|
|
15968
16148
|
//#region ../../libs/pi-extension/src/runtime/execute-pi-task.ts
|
|
15969
16149
|
/**
|
|
15970
16150
|
* executePiTask — run a single Task attempt using pi-coding-agent inside a
|
|
@@ -16013,7 +16193,9 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
16013
16193
|
const task = claimedTask.task;
|
|
16014
16194
|
const attemptN = claimedTask.attemptN;
|
|
16015
16195
|
const startTime = Date.now();
|
|
16016
|
-
const
|
|
16196
|
+
const requestedMountPath = opts.mountPath ?? process.cwd();
|
|
16197
|
+
const executionPlan = opts.makeExecutionPlan?.(claimedTask) ?? null;
|
|
16198
|
+
const workspace = prepareTaskWorkspace(task, requestedMountPath, executionPlan);
|
|
16017
16199
|
const mountPath = workspace.mountPath;
|
|
16018
16200
|
if (reporter.cancelSignal.aborted) return {
|
|
16019
16201
|
taskId: task.id,
|
|
@@ -16229,7 +16411,8 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
16229
16411
|
"moltnet.task.id": task.id,
|
|
16230
16412
|
"moltnet.task.attempt": attemptN,
|
|
16231
16413
|
"moltnet.task.type": task.taskType
|
|
16232
|
-
}
|
|
16414
|
+
},
|
|
16415
|
+
sessionPersistence: executionPlan?.sessionPersistence ?? void 0
|
|
16233
16416
|
});
|
|
16234
16417
|
} catch (err) {
|
|
16235
16418
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -16457,107 +16640,6 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
16457
16640
|
}
|
|
16458
16641
|
}
|
|
16459
16642
|
}
|
|
16460
|
-
function resolveTaskWorktreeBranch(task) {
|
|
16461
|
-
if (taskTypeWorkspaceMode(task.taskType) !== "dedicated_worktree") return null;
|
|
16462
|
-
if (task.taskType === "fulfill_brief") {
|
|
16463
|
-
const input = task.input;
|
|
16464
|
-
const slug = slugifyBranchComponent(typeof input.title === "string" && input.title.trim().length > 0 ? input.title : typeof input.brief === "string" && input.brief.trim().length > 0 ? input.brief : task.taskType) || "task";
|
|
16465
|
-
if (task.correlationId) return `moltnet/${task.correlationId}/${slug}`;
|
|
16466
|
-
return `feat/${(typeof input.scopeHint === "string" && input.scopeHint.trim().length > 0 ? slugifyBranchComponent(input.scopeHint) : "task") || "task"}-${slug}`;
|
|
16467
|
-
}
|
|
16468
|
-
return `task/${slugifyBranchComponent(task.taskType) || "task"}-${task.id.slice(0, 8)}`;
|
|
16469
|
-
}
|
|
16470
|
-
function slugifyBranchComponent(input) {
|
|
16471
|
-
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
|
|
16472
|
-
}
|
|
16473
|
-
function prepareTaskWorkspace(task, requestedMountPath) {
|
|
16474
|
-
const branch = resolveTaskWorktreeBranch(task);
|
|
16475
|
-
if (!branch) return {
|
|
16476
|
-
mountPath: requestedMountPath,
|
|
16477
|
-
mode: "shared_mount",
|
|
16478
|
-
branch: null,
|
|
16479
|
-
cleanup: () => {}
|
|
16480
|
-
};
|
|
16481
|
-
const mainRepo = findMainWorktree();
|
|
16482
|
-
const worktreeDir = join(mainRepo, ".worktrees", `task-${task.id}`);
|
|
16483
|
-
removeExistingTaskWorktree(mainRepo, worktreeDir);
|
|
16484
|
-
const relMount = relative(mainRepo, requestedMountPath);
|
|
16485
|
-
const mountPath = relMount === "" || relMount.startsWith("..") ? worktreeDir : join(worktreeDir, relMount);
|
|
16486
|
-
const baseRef = resolveWorktreeBaseRef(mainRepo);
|
|
16487
|
-
execFileSync("git", gitRefExists(mainRepo, `refs/heads/${branch}`) ? [
|
|
16488
|
-
"-C",
|
|
16489
|
-
mainRepo,
|
|
16490
|
-
"worktree",
|
|
16491
|
-
"add",
|
|
16492
|
-
worktreeDir,
|
|
16493
|
-
branch
|
|
16494
|
-
] : [
|
|
16495
|
-
"-C",
|
|
16496
|
-
mainRepo,
|
|
16497
|
-
"worktree",
|
|
16498
|
-
"add",
|
|
16499
|
-
"-b",
|
|
16500
|
-
branch,
|
|
16501
|
-
worktreeDir,
|
|
16502
|
-
baseRef
|
|
16503
|
-
], { stdio: "pipe" });
|
|
16504
|
-
return {
|
|
16505
|
-
mountPath,
|
|
16506
|
-
mode: "dedicated_worktree",
|
|
16507
|
-
branch,
|
|
16508
|
-
cleanup: () => {
|
|
16509
|
-
execFileSync("git", [
|
|
16510
|
-
"-C",
|
|
16511
|
-
mainRepo,
|
|
16512
|
-
"worktree",
|
|
16513
|
-
"remove",
|
|
16514
|
-
"--force",
|
|
16515
|
-
worktreeDir
|
|
16516
|
-
], { stdio: "pipe" });
|
|
16517
|
-
}
|
|
16518
|
-
};
|
|
16519
|
-
}
|
|
16520
|
-
function removeExistingTaskWorktree(mainRepo, worktreeDir) {
|
|
16521
|
-
if (!existsSync(worktreeDir)) return;
|
|
16522
|
-
const list = execFileSync("git", [
|
|
16523
|
-
"-C",
|
|
16524
|
-
mainRepo,
|
|
16525
|
-
"worktree",
|
|
16526
|
-
"list",
|
|
16527
|
-
"--porcelain"
|
|
16528
|
-
], {
|
|
16529
|
-
encoding: "utf8",
|
|
16530
|
-
stdio: "pipe"
|
|
16531
|
-
});
|
|
16532
|
-
const marker = `worktree ${worktreeDir}\n`;
|
|
16533
|
-
if (!list.includes(marker) && !list.endsWith(`worktree ${worktreeDir}`)) return;
|
|
16534
|
-
execFileSync("git", [
|
|
16535
|
-
"-C",
|
|
16536
|
-
mainRepo,
|
|
16537
|
-
"worktree",
|
|
16538
|
-
"remove",
|
|
16539
|
-
"--force",
|
|
16540
|
-
worktreeDir
|
|
16541
|
-
], { stdio: "pipe" });
|
|
16542
|
-
}
|
|
16543
|
-
function resolveWorktreeBaseRef(mainRepo) {
|
|
16544
|
-
return gitRefExists(mainRepo, "refs/heads/main") ? "main" : "HEAD";
|
|
16545
|
-
}
|
|
16546
|
-
function gitRefExists(mainRepo, ref) {
|
|
16547
|
-
try {
|
|
16548
|
-
execFileSync("git", [
|
|
16549
|
-
"-C",
|
|
16550
|
-
mainRepo,
|
|
16551
|
-
"show-ref",
|
|
16552
|
-
"--verify",
|
|
16553
|
-
"--quiet",
|
|
16554
|
-
ref
|
|
16555
|
-
], { stdio: "pipe" });
|
|
16556
|
-
return true;
|
|
16557
|
-
} catch {
|
|
16558
|
-
return false;
|
|
16559
|
-
}
|
|
16560
|
-
}
|
|
16561
16643
|
function emptyUsage(provider, model) {
|
|
16562
16644
|
return {
|
|
16563
16645
|
inputTokens: 0,
|
|
@@ -16757,6 +16839,419 @@ function createGhCliClient() {
|
|
|
16757
16839
|
};
|
|
16758
16840
|
}
|
|
16759
16841
|
//#endregion
|
|
16842
|
+
//#region src/lib/daemon-slot-registry.ts
|
|
16843
|
+
var DaemonSlotRegistryError = class extends Error {
|
|
16844
|
+
constructor(operation, cause) {
|
|
16845
|
+
super(`Daemon slot registry failed during ${operation}`, { cause });
|
|
16846
|
+
this.operation = operation;
|
|
16847
|
+
this.name = "DaemonSlotRegistryError";
|
|
16848
|
+
}
|
|
16849
|
+
};
|
|
16850
|
+
var DaemonSlotRegistry = class {
|
|
16851
|
+
db;
|
|
16852
|
+
constructor(dbPath) {
|
|
16853
|
+
try {
|
|
16854
|
+
this.db = new DatabaseSync(dbPath);
|
|
16855
|
+
this.withDb("initialize schema", () => {
|
|
16856
|
+
this.db.exec(`
|
|
16857
|
+
PRAGMA journal_mode = WAL;
|
|
16858
|
+
|
|
16859
|
+
CREATE TABLE IF NOT EXISTS daemon_slots (
|
|
16860
|
+
agent_name TEXT NOT NULL,
|
|
16861
|
+
provider TEXT NOT NULL,
|
|
16862
|
+
model TEXT NOT NULL,
|
|
16863
|
+
slot_key TEXT NOT NULL,
|
|
16864
|
+
task_type TEXT NOT NULL,
|
|
16865
|
+
state TEXT NOT NULL CHECK (state IN ('active', 'idle')),
|
|
16866
|
+
last_task_id TEXT NOT NULL,
|
|
16867
|
+
last_attempt_n INTEGER NOT NULL,
|
|
16868
|
+
created_at_ms INTEGER NOT NULL,
|
|
16869
|
+
last_used_at_ms INTEGER NOT NULL,
|
|
16870
|
+
expires_at_ms INTEGER NOT NULL,
|
|
16871
|
+
PRIMARY KEY (agent_name, provider, model, slot_key)
|
|
16872
|
+
);
|
|
16873
|
+
|
|
16874
|
+
CREATE TABLE IF NOT EXISTS daemon_slot_sessions (
|
|
16875
|
+
agent_name TEXT NOT NULL,
|
|
16876
|
+
provider TEXT NOT NULL,
|
|
16877
|
+
model TEXT NOT NULL,
|
|
16878
|
+
slot_key TEXT NOT NULL,
|
|
16879
|
+
session_dir TEXT NOT NULL UNIQUE,
|
|
16880
|
+
session_path TEXT,
|
|
16881
|
+
PRIMARY KEY (agent_name, provider, model, slot_key),
|
|
16882
|
+
FOREIGN KEY (agent_name, provider, model, slot_key)
|
|
16883
|
+
REFERENCES daemon_slots(agent_name, provider, model, slot_key)
|
|
16884
|
+
ON DELETE CASCADE
|
|
16885
|
+
);
|
|
16886
|
+
|
|
16887
|
+
CREATE TABLE IF NOT EXISTS daemon_slot_workspaces (
|
|
16888
|
+
agent_name TEXT NOT NULL,
|
|
16889
|
+
provider TEXT NOT NULL,
|
|
16890
|
+
model TEXT NOT NULL,
|
|
16891
|
+
slot_key TEXT NOT NULL,
|
|
16892
|
+
workspace_id TEXT NOT NULL UNIQUE,
|
|
16893
|
+
worktree_path TEXT NOT NULL,
|
|
16894
|
+
worktree_branch TEXT,
|
|
16895
|
+
PRIMARY KEY (agent_name, provider, model, slot_key),
|
|
16896
|
+
FOREIGN KEY (agent_name, provider, model, slot_key)
|
|
16897
|
+
REFERENCES daemon_slots(agent_name, provider, model, slot_key)
|
|
16898
|
+
ON DELETE CASCADE
|
|
16899
|
+
);
|
|
16900
|
+
|
|
16901
|
+
CREATE INDEX IF NOT EXISTS daemon_slots_expires_idx
|
|
16902
|
+
ON daemon_slots (expires_at_ms);
|
|
16903
|
+
`);
|
|
16904
|
+
});
|
|
16905
|
+
} catch (error) {
|
|
16906
|
+
throw new DaemonSlotRegistryError("open database", error);
|
|
16907
|
+
}
|
|
16908
|
+
}
|
|
16909
|
+
close() {
|
|
16910
|
+
try {
|
|
16911
|
+
this.db.close();
|
|
16912
|
+
} catch (error) {
|
|
16913
|
+
throw new DaemonSlotRegistryError("close database", error);
|
|
16914
|
+
}
|
|
16915
|
+
}
|
|
16916
|
+
beginSlot(input) {
|
|
16917
|
+
const now = Date.now();
|
|
16918
|
+
const expiresAtMs = now + input.ttlSec * 1e3;
|
|
16919
|
+
this.withDb("upsert slot", () => this.db.prepare(`INSERT INTO daemon_slots (
|
|
16920
|
+
agent_name, provider, model, slot_key, task_type, state,
|
|
16921
|
+
last_task_id, last_attempt_n, created_at_ms, last_used_at_ms,
|
|
16922
|
+
expires_at_ms
|
|
16923
|
+
) VALUES (
|
|
16924
|
+
:agentName, :provider, :model, :slotKey, :taskType, 'active',
|
|
16925
|
+
:lastTaskId, :lastAttemptN, :createdAtMs, :lastUsedAtMs,
|
|
16926
|
+
:expiresAtMs
|
|
16927
|
+
)
|
|
16928
|
+
ON CONFLICT(agent_name, provider, model, slot_key) DO UPDATE SET
|
|
16929
|
+
task_type = excluded.task_type,
|
|
16930
|
+
state = 'active',
|
|
16931
|
+
last_task_id = excluded.last_task_id,
|
|
16932
|
+
last_attempt_n = excluded.last_attempt_n,
|
|
16933
|
+
last_used_at_ms = excluded.last_used_at_ms,
|
|
16934
|
+
expires_at_ms = excluded.expires_at_ms`).run({
|
|
16935
|
+
agentName: input.agentName,
|
|
16936
|
+
provider: input.provider,
|
|
16937
|
+
model: input.model,
|
|
16938
|
+
slotKey: input.slotKey,
|
|
16939
|
+
taskType: input.taskType,
|
|
16940
|
+
lastTaskId: input.lastTaskId,
|
|
16941
|
+
lastAttemptN: input.lastAttemptN,
|
|
16942
|
+
createdAtMs: now,
|
|
16943
|
+
lastUsedAtMs: now,
|
|
16944
|
+
expiresAtMs
|
|
16945
|
+
}));
|
|
16946
|
+
if (input.sessionDir) this.withDb("upsert slot session", () => this.db.prepare(`INSERT INTO daemon_slot_sessions (
|
|
16947
|
+
agent_name, provider, model, slot_key, session_dir, session_path
|
|
16948
|
+
) VALUES (
|
|
16949
|
+
:agentName, :provider, :model, :slotKey, :sessionDir, :sessionPath
|
|
16950
|
+
)
|
|
16951
|
+
ON CONFLICT(agent_name, provider, model, slot_key) DO UPDATE SET
|
|
16952
|
+
session_dir = excluded.session_dir,
|
|
16953
|
+
session_path = excluded.session_path`).run({
|
|
16954
|
+
agentName: input.agentName,
|
|
16955
|
+
provider: input.provider,
|
|
16956
|
+
model: input.model,
|
|
16957
|
+
slotKey: input.slotKey,
|
|
16958
|
+
sessionDir: input.sessionDir,
|
|
16959
|
+
sessionPath: input.sessionPath
|
|
16960
|
+
}));
|
|
16961
|
+
if (input.workspaceId && input.worktreePath) this.withDb("upsert slot workspace", () => this.db.prepare(`INSERT INTO daemon_slot_workspaces (
|
|
16962
|
+
agent_name, provider, model, slot_key, workspace_id, worktree_path,
|
|
16963
|
+
worktree_branch
|
|
16964
|
+
) VALUES (
|
|
16965
|
+
:agentName, :provider, :model, :slotKey, :workspaceId, :worktreePath,
|
|
16966
|
+
:worktreeBranch
|
|
16967
|
+
)
|
|
16968
|
+
ON CONFLICT(agent_name, provider, model, slot_key) DO UPDATE SET
|
|
16969
|
+
workspace_id = excluded.workspace_id,
|
|
16970
|
+
worktree_path = excluded.worktree_path,
|
|
16971
|
+
worktree_branch = excluded.worktree_branch`).run({
|
|
16972
|
+
agentName: input.agentName,
|
|
16973
|
+
provider: input.provider,
|
|
16974
|
+
model: input.model,
|
|
16975
|
+
slotKey: input.slotKey,
|
|
16976
|
+
workspaceId: input.workspaceId,
|
|
16977
|
+
worktreePath: input.worktreePath,
|
|
16978
|
+
worktreeBranch: input.worktreeBranch
|
|
16979
|
+
}));
|
|
16980
|
+
}
|
|
16981
|
+
finishSlot(identity, slotKey, ttlSec, sessionPath) {
|
|
16982
|
+
const now = Date.now();
|
|
16983
|
+
this.withDb("finish slot", () => this.db.prepare(`UPDATE daemon_slots
|
|
16984
|
+
SET state = 'idle',
|
|
16985
|
+
last_used_at_ms = ?,
|
|
16986
|
+
expires_at_ms = ?
|
|
16987
|
+
WHERE agent_name = ? AND provider = ? AND model = ? AND slot_key = ?`).run(now, now + ttlSec * 1e3, identity.agentName, identity.provider, identity.model, slotKey));
|
|
16988
|
+
if (sessionPath !== null) this.withDb("update slot session path", () => this.db.prepare(`UPDATE daemon_slot_sessions
|
|
16989
|
+
SET session_path = ?
|
|
16990
|
+
WHERE agent_name = ? AND provider = ? AND model = ? AND slot_key = ?`).run(sessionPath, identity.agentName, identity.provider, identity.model, slotKey));
|
|
16991
|
+
}
|
|
16992
|
+
reapExpiredSlots(now = Date.now()) {
|
|
16993
|
+
this.withDb("begin reap transaction", () => this.db.exec("BEGIN IMMEDIATE"));
|
|
16994
|
+
try {
|
|
16995
|
+
const slots = this.withDb("select expired slots", () => this.db.prepare(`SELECT
|
|
16996
|
+
agent_name as agentName,
|
|
16997
|
+
provider,
|
|
16998
|
+
model,
|
|
16999
|
+
slot_key as slotKey,
|
|
17000
|
+
task_type as taskType,
|
|
17001
|
+
state,
|
|
17002
|
+
last_task_id as lastTaskId,
|
|
17003
|
+
last_attempt_n as lastAttemptN,
|
|
17004
|
+
created_at_ms as createdAtMs,
|
|
17005
|
+
last_used_at_ms as lastUsedAtMs,
|
|
17006
|
+
expires_at_ms as expiresAtMs
|
|
17007
|
+
FROM daemon_slots
|
|
17008
|
+
WHERE expires_at_ms <= ?`).all(now));
|
|
17009
|
+
if (slots.length < 1) {
|
|
17010
|
+
this.withDb("commit empty reap transaction", () => this.db.exec("COMMIT"));
|
|
17011
|
+
return [];
|
|
17012
|
+
}
|
|
17013
|
+
const selectSession = this.withDb("prepare slot session lookup", () => this.db.prepare(`SELECT
|
|
17014
|
+
agent_name as agentName,
|
|
17015
|
+
provider,
|
|
17016
|
+
model,
|
|
17017
|
+
slot_key as slotKey,
|
|
17018
|
+
session_dir as sessionDir,
|
|
17019
|
+
session_path as sessionPath
|
|
17020
|
+
FROM daemon_slot_sessions
|
|
17021
|
+
WHERE agent_name = ? AND provider = ? AND model = ? AND slot_key = ?`));
|
|
17022
|
+
const selectWorkspace = this.withDb("prepare slot workspace lookup", () => this.db.prepare(`SELECT
|
|
17023
|
+
agent_name as agentName,
|
|
17024
|
+
provider,
|
|
17025
|
+
model,
|
|
17026
|
+
slot_key as slotKey,
|
|
17027
|
+
workspace_id as workspaceId,
|
|
17028
|
+
worktree_path as worktreePath,
|
|
17029
|
+
worktree_branch as worktreeBranch
|
|
17030
|
+
FROM daemon_slot_workspaces
|
|
17031
|
+
WHERE agent_name = ? AND provider = ? AND model = ? AND slot_key = ?`));
|
|
17032
|
+
const deleteStmt = this.withDb("prepare expired slot delete", () => this.db.prepare("DELETE FROM daemon_slots WHERE agent_name = ? AND provider = ? AND model = ? AND slot_key = ?"));
|
|
17033
|
+
const out = [];
|
|
17034
|
+
for (const slot of slots) {
|
|
17035
|
+
const session = this.withDb("select slot session", () => selectSession.get(slot.agentName, slot.provider, slot.model, slot.slotKey) ?? null);
|
|
17036
|
+
const workspace = this.withDb("select slot workspace", () => selectWorkspace.get(slot.agentName, slot.provider, slot.model, slot.slotKey) ?? null);
|
|
17037
|
+
out.push({
|
|
17038
|
+
slot,
|
|
17039
|
+
session,
|
|
17040
|
+
workspace
|
|
17041
|
+
});
|
|
17042
|
+
this.withDb("delete expired slot", () => deleteStmt.run(slot.agentName, slot.provider, slot.model, slot.slotKey));
|
|
17043
|
+
}
|
|
17044
|
+
this.withDb("commit reap transaction", () => this.db.exec("COMMIT"));
|
|
17045
|
+
for (const item of out) {
|
|
17046
|
+
if (item.session) cleanupPiSessionDir(item.session.sessionDir);
|
|
17047
|
+
if (item.workspace) cleanupReusableWorktree(item.workspace.worktreePath);
|
|
17048
|
+
}
|
|
17049
|
+
return out;
|
|
17050
|
+
} catch (error) {
|
|
17051
|
+
try {
|
|
17052
|
+
this.db.exec("ROLLBACK");
|
|
17053
|
+
} catch {}
|
|
17054
|
+
throw error instanceof DaemonSlotRegistryError ? error : new DaemonSlotRegistryError("reap expired slots", error);
|
|
17055
|
+
}
|
|
17056
|
+
}
|
|
17057
|
+
withDb(operation, fn) {
|
|
17058
|
+
try {
|
|
17059
|
+
return fn();
|
|
17060
|
+
} catch (error) {
|
|
17061
|
+
throw new DaemonSlotRegistryError(operation, error);
|
|
17062
|
+
}
|
|
17063
|
+
}
|
|
17064
|
+
};
|
|
17065
|
+
function resolveLatestPiSessionPath(sessionDir) {
|
|
17066
|
+
try {
|
|
17067
|
+
const latestEntry = readdirSync(sessionDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => entry.name).sort().at(-1);
|
|
17068
|
+
return latestEntry ? join(sessionDir, latestEntry) : null;
|
|
17069
|
+
} catch {
|
|
17070
|
+
return null;
|
|
17071
|
+
}
|
|
17072
|
+
}
|
|
17073
|
+
function cleanupPiSessionDir(sessionDir) {
|
|
17074
|
+
rmSync(sessionDir, {
|
|
17075
|
+
recursive: true,
|
|
17076
|
+
force: true
|
|
17077
|
+
});
|
|
17078
|
+
}
|
|
17079
|
+
function cleanupReusableWorktree(worktreePath) {
|
|
17080
|
+
const mainRepo = findMainWorktree();
|
|
17081
|
+
if (isRegisteredWorktree(mainRepo, worktreePath)) {
|
|
17082
|
+
execFileSync("git", [
|
|
17083
|
+
"-C",
|
|
17084
|
+
mainRepo,
|
|
17085
|
+
"worktree",
|
|
17086
|
+
"remove",
|
|
17087
|
+
"--force",
|
|
17088
|
+
worktreePath
|
|
17089
|
+
], { stdio: "pipe" });
|
|
17090
|
+
return;
|
|
17091
|
+
}
|
|
17092
|
+
rmSync(worktreePath, {
|
|
17093
|
+
recursive: true,
|
|
17094
|
+
force: true
|
|
17095
|
+
});
|
|
17096
|
+
}
|
|
17097
|
+
function isRegisteredWorktree(mainRepo, worktreeDir) {
|
|
17098
|
+
const list = execFileSync("git", [
|
|
17099
|
+
"-C",
|
|
17100
|
+
mainRepo,
|
|
17101
|
+
"worktree",
|
|
17102
|
+
"list",
|
|
17103
|
+
"--porcelain"
|
|
17104
|
+
], {
|
|
17105
|
+
encoding: "utf8",
|
|
17106
|
+
stdio: "pipe"
|
|
17107
|
+
});
|
|
17108
|
+
const marker = `worktree ${worktreeDir}\n`;
|
|
17109
|
+
return list.includes(marker) || list.endsWith(`worktree ${worktreeDir}`);
|
|
17110
|
+
}
|
|
17111
|
+
//#endregion
|
|
17112
|
+
//#region src/lib/slugify.ts
|
|
17113
|
+
function slugifyAsciiLower(input, maxLen, preserveChars = []) {
|
|
17114
|
+
const preserved = new Set(preserveChars.map((char) => char.toLowerCase()));
|
|
17115
|
+
let out = "";
|
|
17116
|
+
let pendingDash = false;
|
|
17117
|
+
for (const rawChar of input) {
|
|
17118
|
+
const char = rawChar.toLowerCase();
|
|
17119
|
+
const isAlphaNum = char >= "a" && char <= "z" || char >= "0" && char <= "9";
|
|
17120
|
+
const isPreserved = preserved.has(char);
|
|
17121
|
+
if (isAlphaNum || isPreserved) {
|
|
17122
|
+
if (pendingDash && out.length > 0 && out.length < maxLen) out += "-";
|
|
17123
|
+
pendingDash = false;
|
|
17124
|
+
if (out.length < maxLen) out += char;
|
|
17125
|
+
else break;
|
|
17126
|
+
continue;
|
|
17127
|
+
}
|
|
17128
|
+
pendingDash = out.length > 0;
|
|
17129
|
+
}
|
|
17130
|
+
return out;
|
|
17131
|
+
}
|
|
17132
|
+
//#endregion
|
|
17133
|
+
//#region src/lib/session-policy.ts
|
|
17134
|
+
/**
|
|
17135
|
+
* Resolve the daemon-facing execution policy for a task and, when the task
|
|
17136
|
+
* type permits warm reuse, compute the daemon-local `sessionKey`.
|
|
17137
|
+
*
|
|
17138
|
+
* `correlationId` remains the audit/query key; `sessionKey` is narrower and
|
|
17139
|
+
* may fold in task-type-specific discriminators so eval/review flows stay
|
|
17140
|
+
* isolated even inside one correlation group.
|
|
17141
|
+
*/
|
|
17142
|
+
function deriveTaskSessionDescriptor(task) {
|
|
17143
|
+
const policy = getTaskExecutionPolicy(task.taskType);
|
|
17144
|
+
return {
|
|
17145
|
+
policy,
|
|
17146
|
+
sessionKey: buildTaskSessionKey(task, policy)
|
|
17147
|
+
};
|
|
17148
|
+
}
|
|
17149
|
+
function buildTaskSessionKey(task, policy) {
|
|
17150
|
+
if (!policy.resumable) return null;
|
|
17151
|
+
switch (policy.sessionScope) {
|
|
17152
|
+
case "none": return null;
|
|
17153
|
+
case "correlation": return task.correlationId ? `${task.taskType}:correlation:${task.correlationId}` : null;
|
|
17154
|
+
case "custom": return buildCustomSessionKey(task);
|
|
17155
|
+
}
|
|
17156
|
+
}
|
|
17157
|
+
function buildCustomSessionKey(task) {
|
|
17158
|
+
switch (task.taskType) {
|
|
17159
|
+
case "run_eval": {
|
|
17160
|
+
const variantLabel = typeof task.input.variantLabel === "string" ? task.input.variantLabel : null;
|
|
17161
|
+
if (!task.correlationId || !variantLabel) return null;
|
|
17162
|
+
return `run_eval:correlation:${task.correlationId}:variant:${slugifySessionComponent(variantLabel)}`;
|
|
17163
|
+
}
|
|
17164
|
+
case "judge_eval_variant": {
|
|
17165
|
+
const runTaskIds = Array.isArray(task.input.runTaskIds) ? task.input.runTaskIds.filter((value) => typeof value === "string") : [];
|
|
17166
|
+
if (runTaskIds.length < 1) return null;
|
|
17167
|
+
return `judge_eval_variant:run_tasks:${[...runTaskIds].sort().join(",")}`;
|
|
17168
|
+
}
|
|
17169
|
+
default: return null;
|
|
17170
|
+
}
|
|
17171
|
+
}
|
|
17172
|
+
function slugifySessionComponent(input) {
|
|
17173
|
+
return slugifyAsciiLower(input.trim(), 64);
|
|
17174
|
+
}
|
|
17175
|
+
//#endregion
|
|
17176
|
+
//#region src/lib/task-execution-plan.ts
|
|
17177
|
+
function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmSessionTtlSec) {
|
|
17178
|
+
const descriptor = deriveTaskSessionDescriptor(task);
|
|
17179
|
+
const slotKey = warmSessionTtlSec > 0 ? descriptor.sessionKey : null;
|
|
17180
|
+
const workspaceScope = slotKey !== null ? descriptor.policy.workspaceScope : "attempt";
|
|
17181
|
+
const slotId = slotKey ? buildDaemonSlotId(identity, slotKey) : null;
|
|
17182
|
+
const sessionDir = slotId ? `${stateDirs.piSessionsDir}/${encodeURIComponent(slotId)}` : null;
|
|
17183
|
+
const worktreeBranch = resolveTaskWorktreeBranch(task, descriptor.policy);
|
|
17184
|
+
const workspaceId = worktreeBranch !== null ? resolveTaskWorkspaceId(task, {
|
|
17185
|
+
sessionKey: slotId,
|
|
17186
|
+
workspaceScope,
|
|
17187
|
+
sessionPersistence: sessionDir ? { sessionDir } : null
|
|
17188
|
+
}) : null;
|
|
17189
|
+
return {
|
|
17190
|
+
descriptor,
|
|
17191
|
+
sessionKey: slotId,
|
|
17192
|
+
slotKey,
|
|
17193
|
+
slotId,
|
|
17194
|
+
workspaceScope,
|
|
17195
|
+
sessionPersistence: sessionDir ? { sessionDir } : null,
|
|
17196
|
+
workspaceId,
|
|
17197
|
+
worktreeBranch
|
|
17198
|
+
};
|
|
17199
|
+
}
|
|
17200
|
+
function buildDaemonSlotId(identity, slotKey) {
|
|
17201
|
+
return [
|
|
17202
|
+
"agent",
|
|
17203
|
+
slugSlotIdentityComponent(identity.agentName),
|
|
17204
|
+
"provider",
|
|
17205
|
+
slugSlotIdentityComponent(identity.provider),
|
|
17206
|
+
"model",
|
|
17207
|
+
slugSlotIdentityComponent(identity.model),
|
|
17208
|
+
"key",
|
|
17209
|
+
slotKey
|
|
17210
|
+
].join(":");
|
|
17211
|
+
}
|
|
17212
|
+
function slugSlotIdentityComponent(input) {
|
|
17213
|
+
return slugifyAsciiLower(input.trim(), 64, [
|
|
17214
|
+
".",
|
|
17215
|
+
"_",
|
|
17216
|
+
"-"
|
|
17217
|
+
]);
|
|
17218
|
+
}
|
|
17219
|
+
function resolveTaskWorktreeBranch(task, policy) {
|
|
17220
|
+
if (policy.workspaceMode !== "dedicated_worktree") return null;
|
|
17221
|
+
if (task.taskType === "fulfill_brief") {
|
|
17222
|
+
const input = task.input;
|
|
17223
|
+
const slug = slugifyAsciiLower(typeof input.title === "string" && input.title.trim().length > 0 ? input.title : typeof input.brief === "string" && input.brief.trim().length > 0 ? input.brief : task.taskType, 60) || "task";
|
|
17224
|
+
if (task.correlationId) return `moltnet/${task.correlationId}/${slug}`;
|
|
17225
|
+
return `feat/${(typeof input.scopeHint === "string" && input.scopeHint.trim().length > 0 ? slugifyAsciiLower(input.scopeHint, 60) : "task") || "task"}-${slug}`;
|
|
17226
|
+
}
|
|
17227
|
+
return `task/${slugifyAsciiLower(task.taskType, 60) || "task"}-${task.id.slice(0, 8)}`;
|
|
17228
|
+
}
|
|
17229
|
+
function resolveTaskWorkspaceId(task, executionPlan) {
|
|
17230
|
+
if (executionPlan.workspaceScope === "session" && executionPlan.sessionKey !== null) return `session-${encodeURIComponent(executionPlan.sessionKey)}`;
|
|
17231
|
+
return `task-${task.id}`;
|
|
17232
|
+
}
|
|
17233
|
+
//#endregion
|
|
17234
|
+
//#region src/lib/execution-plan-cache.ts
|
|
17235
|
+
function createExecutionPlanCache(args) {
|
|
17236
|
+
const cache = /* @__PURE__ */ new Map();
|
|
17237
|
+
return {
|
|
17238
|
+
getOrCreate(claimedTask) {
|
|
17239
|
+
const key = buildClaimedTaskKey(claimedTask);
|
|
17240
|
+
const existing = cache.get(key);
|
|
17241
|
+
if (existing) return existing;
|
|
17242
|
+
const plan = buildDaemonTaskExecutionPlan(claimedTask.task, args.stateDirs, args.slotIdentity, args.warmSessionTtlSec);
|
|
17243
|
+
cache.set(key, plan);
|
|
17244
|
+
return plan;
|
|
17245
|
+
},
|
|
17246
|
+
delete(claimedTask) {
|
|
17247
|
+
cache.delete(buildClaimedTaskKey(claimedTask));
|
|
17248
|
+
}
|
|
17249
|
+
};
|
|
17250
|
+
}
|
|
17251
|
+
function buildClaimedTaskKey(task) {
|
|
17252
|
+
return `${task.task.id}:${task.attemptN}`;
|
|
17253
|
+
}
|
|
17254
|
+
//#endregion
|
|
16760
17255
|
//#region src/lib/finalize.ts
|
|
16761
17256
|
async function finalizeTask(agent, output, ctx = {}) {
|
|
16762
17257
|
if (output.status === "cancelled") return;
|
|
@@ -16841,7 +17336,8 @@ var DEFAULTS = {
|
|
|
16841
17336
|
maxBatchSize: 50,
|
|
16842
17337
|
flushIntervalMs: 200,
|
|
16843
17338
|
maxTurns: 0,
|
|
16844
|
-
maxBashTimeouts: 3
|
|
17339
|
+
maxBashTimeouts: 3,
|
|
17340
|
+
warmSessionTtlSec: 1800
|
|
16845
17341
|
};
|
|
16846
17342
|
var MissingRequiredOptionError = class extends Error {
|
|
16847
17343
|
constructor(flag) {
|
|
@@ -16865,6 +17361,7 @@ function parseCommonOptions(args) {
|
|
|
16865
17361
|
flushIntervalMs: parseNonNegativeInt(args["flush-interval-ms"], "flush-interval-ms", DEFAULTS.flushIntervalMs),
|
|
16866
17362
|
maxTurns: parseNonNegativeInt(args["max-turns"], "max-turns", DEFAULTS.maxTurns),
|
|
16867
17363
|
maxBashTimeouts: parseNonNegativeInt(args["max-bash-timeouts"], "max-bash-timeouts", DEFAULTS.maxBashTimeouts),
|
|
17364
|
+
warmSessionTtlSec: parseNonNegativeInt(args["warm-session-ttl-sec"], "warm-session-ttl-sec", DEFAULTS.warmSessionTtlSec),
|
|
16868
17365
|
debug: args.debug === true
|
|
16869
17366
|
};
|
|
16870
17367
|
}
|
|
@@ -16900,6 +17397,7 @@ function commonOptionDefs() {
|
|
|
16900
17397
|
"flush-interval-ms": { type: "string" },
|
|
16901
17398
|
"max-turns": { type: "string" },
|
|
16902
17399
|
"max-bash-timeouts": { type: "string" },
|
|
17400
|
+
"warm-session-ttl-sec": { type: "string" },
|
|
16903
17401
|
debug: { type: "boolean" }
|
|
16904
17402
|
};
|
|
16905
17403
|
}
|
|
@@ -16982,6 +17480,18 @@ function findUp(startDir, filename) {
|
|
|
16982
17480
|
}
|
|
16983
17481
|
}
|
|
16984
17482
|
//#endregion
|
|
17483
|
+
//#region src/lib/state-dir.ts
|
|
17484
|
+
function ensureDaemonStateDirs(mountPath) {
|
|
17485
|
+
const rootDir = join(mountPath, ".moltnet", "d");
|
|
17486
|
+
const piSessionsDir = join(rootDir, "pi-sessions");
|
|
17487
|
+
mkdirSync(piSessionsDir, { recursive: true });
|
|
17488
|
+
return {
|
|
17489
|
+
rootDir,
|
|
17490
|
+
piSessionsDir,
|
|
17491
|
+
registryDbPath: join(rootDir, "daemon-state.sqlite")
|
|
17492
|
+
};
|
|
17493
|
+
}
|
|
17494
|
+
//#endregion
|
|
16985
17495
|
//#region src/lib/turn-event-logger.ts
|
|
16986
17496
|
function makeTurnEventHandler(base, context = {}) {
|
|
16987
17497
|
const log = base.child({
|
|
@@ -17053,6 +17563,19 @@ async function runPolling(opts) {
|
|
|
17053
17563
|
const listLimit = optionalPositiveInt(values["list-limit"], "list-limit", 10);
|
|
17054
17564
|
if (taskTypes.length === 0) console.error(`[${opts.modeLabel}] --task-types is empty — daemon will accept any registered type. Pass an explicit list to limit scope (e.g. --task-types fulfill_brief).`);
|
|
17055
17565
|
const sandbox = resolveSandbox(process.cwd(), values.sandbox);
|
|
17566
|
+
const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
|
|
17567
|
+
const slotRegistry = new DaemonSlotRegistry(stateDirs.registryDbPath);
|
|
17568
|
+
const slotIdentity = {
|
|
17569
|
+
agentName: common.agent,
|
|
17570
|
+
provider: common.provider,
|
|
17571
|
+
model: common.model
|
|
17572
|
+
};
|
|
17573
|
+
const mainRepo = findMainWorktree();
|
|
17574
|
+
const executionPlans = createExecutionPlanCache({
|
|
17575
|
+
stateDirs,
|
|
17576
|
+
slotIdentity,
|
|
17577
|
+
warmSessionTtlSec: common.warmSessionTtlSec
|
|
17578
|
+
});
|
|
17056
17579
|
const ctx = await resolveAgentContext(common.agent);
|
|
17057
17580
|
const cfg = loadConfig();
|
|
17058
17581
|
const otelShutdown = await initWorkerOtel({
|
|
@@ -17103,6 +17626,7 @@ async function runPolling(opts) {
|
|
|
17103
17626
|
provider: common.provider,
|
|
17104
17627
|
model: common.model,
|
|
17105
17628
|
sandboxConfig: sandbox.config,
|
|
17629
|
+
makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
|
|
17106
17630
|
makeOnTurnEvent: makeTurnEventHandlerFactory(rootLogger),
|
|
17107
17631
|
maxTurns: common.maxTurns,
|
|
17108
17632
|
maxBashTimeouts: common.maxBashTimeouts
|
|
@@ -17141,6 +17665,26 @@ async function runPolling(opts) {
|
|
|
17141
17665
|
log: (msg, err) => rootLogger.warn({ err }, msg)
|
|
17142
17666
|
}),
|
|
17143
17667
|
executeTask: async (claimedTask, reporter) => {
|
|
17668
|
+
const executionPlan = executionPlans.getOrCreate(claimedTask);
|
|
17669
|
+
const sessionDescriptor = executionPlan.descriptor;
|
|
17670
|
+
const expired = slotRegistry.reapExpiredSlots();
|
|
17671
|
+
if (expired.length > 0) rootLogger.info({
|
|
17672
|
+
expiredCount: expired.length,
|
|
17673
|
+
slotKeys: expired.map((item) => item.slot.slotKey)
|
|
17674
|
+
}, "agent-daemon.daemon_slots_reaped");
|
|
17675
|
+
rootLogger.debug({
|
|
17676
|
+
taskId: claimedTask.task.id,
|
|
17677
|
+
taskType: claimedTask.task.taskType,
|
|
17678
|
+
resumable: sessionDescriptor.policy.resumable,
|
|
17679
|
+
workspaceMode: sessionDescriptor.policy.workspaceMode,
|
|
17680
|
+
workspaceScope: sessionDescriptor.policy.workspaceScope,
|
|
17681
|
+
sessionScope: sessionDescriptor.policy.sessionScope,
|
|
17682
|
+
slotKey: executionPlan.slotKey,
|
|
17683
|
+
slotId: executionPlan.slotId,
|
|
17684
|
+
sessionKey: executionPlan.sessionKey,
|
|
17685
|
+
piSessionDir: executionPlan.sessionPersistence?.sessionDir ?? null,
|
|
17686
|
+
workspaceId: executionPlan.workspaceId
|
|
17687
|
+
}, "agent-daemon.task_execution_policy");
|
|
17144
17688
|
if (taskTypes.length > 0 && !taskTypes.includes(claimedTask.task.taskType)) return {
|
|
17145
17689
|
taskId: claimedTask.task.id,
|
|
17146
17690
|
attemptN: claimedTask.attemptN,
|
|
@@ -17175,7 +17719,25 @@ async function runPolling(opts) {
|
|
|
17175
17719
|
retryable: false
|
|
17176
17720
|
}
|
|
17177
17721
|
};
|
|
17178
|
-
|
|
17722
|
+
if (executionPlan.slotKey && executionPlan.sessionPersistence) slotRegistry.beginSlot({
|
|
17723
|
+
...slotIdentity,
|
|
17724
|
+
slotKey: executionPlan.slotKey,
|
|
17725
|
+
taskType: claimedTask.task.taskType,
|
|
17726
|
+
sessionDir: executionPlan.sessionPersistence.sessionDir,
|
|
17727
|
+
sessionPath: resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir),
|
|
17728
|
+
workspaceId: executionPlan.workspaceId,
|
|
17729
|
+
worktreePath: executionPlan.workspaceId ? resolveTaskWorktreePath(mainRepo, executionPlan.workspaceId) : null,
|
|
17730
|
+
worktreeBranch: executionPlan.worktreeBranch,
|
|
17731
|
+
lastTaskId: claimedTask.task.id,
|
|
17732
|
+
lastAttemptN: claimedTask.attemptN,
|
|
17733
|
+
ttlSec: common.warmSessionTtlSec
|
|
17734
|
+
});
|
|
17735
|
+
try {
|
|
17736
|
+
return await executeTask(claimedTask, reporter);
|
|
17737
|
+
} finally {
|
|
17738
|
+
executionPlans.delete(claimedTask);
|
|
17739
|
+
if (executionPlan.slotKey) slotRegistry.finishSlot(slotIdentity, executionPlan.slotKey, common.warmSessionTtlSec, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null);
|
|
17740
|
+
}
|
|
17179
17741
|
}
|
|
17180
17742
|
});
|
|
17181
17743
|
const drained = await runtime.start();
|
|
@@ -17183,6 +17745,7 @@ async function runPolling(opts) {
|
|
|
17183
17745
|
rootLogger.info({ processed: drained.length }, "agent-daemon.drained");
|
|
17184
17746
|
return drained.some((o) => o.status !== "completed") ? 1 : 0;
|
|
17185
17747
|
} finally {
|
|
17748
|
+
slotRegistry.close();
|
|
17186
17749
|
await otelShutdown();
|
|
17187
17750
|
await shutdownLogger();
|
|
17188
17751
|
}
|
|
@@ -17243,6 +17806,19 @@ async function runOnce(argv) {
|
|
|
17243
17806
|
throw err;
|
|
17244
17807
|
}
|
|
17245
17808
|
const sandbox = resolveSandbox(process.cwd(), values.sandbox);
|
|
17809
|
+
const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
|
|
17810
|
+
const slotRegistry = new DaemonSlotRegistry(stateDirs.registryDbPath);
|
|
17811
|
+
const slotIdentity = {
|
|
17812
|
+
agentName: opts.agent,
|
|
17813
|
+
provider: opts.provider,
|
|
17814
|
+
model: opts.model
|
|
17815
|
+
};
|
|
17816
|
+
const mainRepo = findMainWorktree();
|
|
17817
|
+
const executionPlans = createExecutionPlanCache({
|
|
17818
|
+
stateDirs,
|
|
17819
|
+
slotIdentity,
|
|
17820
|
+
warmSessionTtlSec: opts.warmSessionTtlSec
|
|
17821
|
+
});
|
|
17246
17822
|
const ctx = await resolveAgentContext(opts.agent);
|
|
17247
17823
|
const cfg = loadConfig();
|
|
17248
17824
|
const otelShutdown = await initWorkerOtel({
|
|
@@ -17291,16 +17867,44 @@ async function runOnce(argv) {
|
|
|
17291
17867
|
onSignal("SIGTERM");
|
|
17292
17868
|
});
|
|
17293
17869
|
try {
|
|
17294
|
-
const
|
|
17870
|
+
const rawExecuteTask = createPiTaskExecutor({
|
|
17295
17871
|
agentName: opts.agent,
|
|
17296
17872
|
mountPath: sandbox.rootDir,
|
|
17297
17873
|
provider: opts.provider,
|
|
17298
17874
|
model: opts.model,
|
|
17299
17875
|
sandboxConfig: sandbox.config,
|
|
17876
|
+
makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
|
|
17300
17877
|
onTurnEvent: makeTurnEventHandler(rootLogger, { taskId }),
|
|
17301
17878
|
maxTurns: opts.maxTurns,
|
|
17302
17879
|
maxBashTimeouts: opts.maxBashTimeouts
|
|
17303
17880
|
});
|
|
17881
|
+
const executeTask = async (claimedTask, reporter) => {
|
|
17882
|
+
const expired = slotRegistry.reapExpiredSlots();
|
|
17883
|
+
if (expired.length > 0) rootLogger.info({
|
|
17884
|
+
expiredCount: expired.length,
|
|
17885
|
+
slotKeys: expired.map((item) => item.slot.slotKey)
|
|
17886
|
+
}, "agent-daemon.daemon_slots_reaped");
|
|
17887
|
+
const executionPlan = executionPlans.getOrCreate(claimedTask);
|
|
17888
|
+
if (executionPlan.slotKey && executionPlan.sessionPersistence) slotRegistry.beginSlot({
|
|
17889
|
+
...slotIdentity,
|
|
17890
|
+
slotKey: executionPlan.slotKey,
|
|
17891
|
+
taskType: claimedTask.task.taskType,
|
|
17892
|
+
sessionDir: executionPlan.sessionPersistence.sessionDir,
|
|
17893
|
+
sessionPath: resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir),
|
|
17894
|
+
workspaceId: executionPlan.workspaceId,
|
|
17895
|
+
worktreePath: executionPlan.workspaceId ? resolveTaskWorktreePath(mainRepo, executionPlan.workspaceId) : null,
|
|
17896
|
+
worktreeBranch: executionPlan.worktreeBranch,
|
|
17897
|
+
lastTaskId: claimedTask.task.id,
|
|
17898
|
+
lastAttemptN: claimedTask.attemptN,
|
|
17899
|
+
ttlSec: opts.warmSessionTtlSec
|
|
17900
|
+
});
|
|
17901
|
+
try {
|
|
17902
|
+
return await rawExecuteTask(claimedTask, reporter);
|
|
17903
|
+
} finally {
|
|
17904
|
+
executionPlans.delete(claimedTask);
|
|
17905
|
+
if (executionPlan.slotKey) slotRegistry.finishSlot(slotIdentity, executionPlan.slotKey, opts.warmSessionTtlSec, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null);
|
|
17906
|
+
}
|
|
17907
|
+
};
|
|
17304
17908
|
const writeCorrelationAnchors = makePrBodyAnchorWriter({
|
|
17305
17909
|
gh: createGhCliClient(),
|
|
17306
17910
|
logger: rootLogger
|
|
@@ -17335,6 +17939,7 @@ async function runOnce(argv) {
|
|
|
17335
17939
|
console.log(JSON.stringify(output, null, 2));
|
|
17336
17940
|
return output.status === "completed" ? 0 : 1;
|
|
17337
17941
|
} finally {
|
|
17942
|
+
slotRegistry.close();
|
|
17338
17943
|
await otelShutdown();
|
|
17339
17944
|
await shutdownLogger();
|
|
17340
17945
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/agent-daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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.",
|
|
@@ -33,19 +33,19 @@
|
|
|
33
33
|
"@opentelemetry/semantic-conventions": "^1.39.0",
|
|
34
34
|
"pino": "^10.3.1",
|
|
35
35
|
"pino-pretty": "^13.1.3",
|
|
36
|
+
"@themoltnet/pi-extension": "0.17.0",
|
|
36
37
|
"@themoltnet/agent-runtime": "0.15.1",
|
|
37
|
-
"@themoltnet/sdk": "0.102.0"
|
|
38
|
-
"@themoltnet/pi-extension": "0.16.2"
|
|
38
|
+
"@themoltnet/sdk": "0.102.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"tsx": "^4.7.0",
|
|
42
42
|
"typescript": "^5.3.3",
|
|
43
43
|
"vite": "^8.0.0",
|
|
44
44
|
"vitest": "^3.0.0",
|
|
45
|
-
"@moltnet/crypto-service": "0.1.0",
|
|
46
45
|
"@moltnet/bootstrap": "0.1.0",
|
|
47
46
|
"@moltnet/database": "0.1.0",
|
|
48
|
-
"@moltnet/tasks": "0.1.0"
|
|
47
|
+
"@moltnet/tasks": "0.1.0",
|
|
48
|
+
"@moltnet/crypto-service": "0.1.0"
|
|
49
49
|
},
|
|
50
50
|
"nx": {
|
|
51
51
|
"tags": [
|