@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.
Files changed (2) hide show
  1. package/dist/main.js +715 -110
  2. 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 in-memory `AgentSession`. The caller is responsible for
15333
- * eventually invoking `session.prompt(...)` and for tearing down — the
15334
- * helper does no lifecycle management beyond construction.
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: SessionManager.inMemory(),
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 workspace = prepareTaskWorkspace(task, opts.mountPath ?? process.cwd());
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
- return executeTask(claimedTask, reporter);
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 executeTask = createPiTaskExecutor({
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.6.2",
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": [