@themoltnet/agent-daemon 0.6.2 → 0.8.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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/main.js +910 -257
  3. package/package.json +6 -6
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";
@@ -3044,10 +3045,10 @@ function validateRubricWeights(rubric) {
3044
3045
  * complementary places.
3045
3046
  *
3046
3047
  * Before this envelope existed, criteria were scattered: a vestigial
3047
- * `criteriaCid` column nobody resolved, an `acceptanceCriteria: string[]`
3048
- * field on `fulfill_brief.input` that was "interpreted by the claiming
3049
- * agent," and inline `rubric` / `criteria[]` fields on judgment-task
3050
- * inputs. None of those were machine-verifiable end-to-end.
3048
+ * `criteriaCid` column nobody resolved, free-form prose on
3049
+ * `fulfill_brief.input`, and inline `rubric` / `criteria[]` fields on
3050
+ * judgment-task inputs. None of those were machine-verifiable
3051
+ * end-to-end.
3051
3052
  *
3052
3053
  * This module defines a single, content-addressable envelope an imposer
3053
3054
  * attaches to any task type. It has four orthogonal sections — pick
@@ -4184,7 +4185,6 @@ var FULFILL_BRIEF_TYPE = "fulfill_brief";
4184
4185
  var FulfillBriefInput = Type$2.Object({
4185
4186
  brief: Type$2.String({ minLength: 1 }),
4186
4187
  title: Type$2.Optional(Type$2.String()),
4187
- acceptanceCriteria: Type$2.Optional(Type$2.Array(Type$2.String())),
4188
4188
  successCriteria: Type$2.Optional(SuccessCriteria),
4189
4189
  seedFiles: Type$2.Optional(Type$2.Array(Type$2.String())),
4190
4190
  scopeHint: Type$2.Optional(Type$2.String())
@@ -4756,7 +4756,10 @@ var BUILT_IN_TASK_TYPES = {
4756
4756
  inputSchema: FulfillBriefInput,
4757
4757
  outputSchema: FulfillBriefOutput,
4758
4758
  outputKind: "artifact",
4759
+ resumable: true,
4759
4760
  workspaceMode: "dedicated_worktree",
4761
+ workspaceScope: "session",
4762
+ sessionScope: "correlation",
4760
4763
  requiresReferences: false,
4761
4764
  validateOutput: requireVerificationWhenCriteriaPresent
4762
4765
  },
@@ -4766,6 +4769,8 @@ var BUILT_IN_TASK_TYPES = {
4766
4769
  outputSchema: AssessBriefOutput,
4767
4770
  outputKind: "judgment",
4768
4771
  workspaceMode: "dedicated_worktree",
4772
+ workspaceScope: "attempt",
4773
+ sessionScope: "none",
4769
4774
  requiresReferences: true,
4770
4775
  validateInput: validateJudgmentInput,
4771
4776
  validateInputAsync: validateAssessBriefInputAsync
@@ -4775,6 +4780,8 @@ var BUILT_IN_TASK_TYPES = {
4775
4780
  inputSchema: CuratePackInput,
4776
4781
  outputSchema: CuratePackOutput,
4777
4782
  outputKind: "artifact",
4783
+ workspaceScope: "attempt",
4784
+ sessionScope: "none",
4778
4785
  requiresReferences: false,
4779
4786
  validateOutput: requireVerificationWhenCriteriaPresent
4780
4787
  },
@@ -4783,6 +4790,8 @@ var BUILT_IN_TASK_TYPES = {
4783
4790
  inputSchema: RenderPackInput,
4784
4791
  outputSchema: RenderPackOutput,
4785
4792
  outputKind: "artifact",
4793
+ workspaceScope: "attempt",
4794
+ sessionScope: "none",
4786
4795
  requiresReferences: false,
4787
4796
  validateOutput: requireVerificationWhenCriteriaPresent,
4788
4797
  validateInputAsync: validateRenderPackInputAsync
@@ -4792,6 +4801,8 @@ var BUILT_IN_TASK_TYPES = {
4792
4801
  inputSchema: JudgePackInput,
4793
4802
  outputSchema: JudgePackOutput,
4794
4803
  outputKind: "judgment",
4804
+ workspaceScope: "attempt",
4805
+ sessionScope: "none",
4795
4806
  requiresReferences: true,
4796
4807
  validateInput: validateJudgmentInput,
4797
4808
  validateOutput: validateJudgePackOutput,
@@ -4802,6 +4813,8 @@ var BUILT_IN_TASK_TYPES = {
4802
4813
  inputSchema: RunEvalInput,
4803
4814
  outputSchema: RunEvalOutput,
4804
4815
  outputKind: "artifact",
4816
+ workspaceScope: "attempt",
4817
+ sessionScope: "custom",
4805
4818
  requiresReferences: false,
4806
4819
  validateOutput: validateRunEvalOutput
4807
4820
  },
@@ -4810,6 +4823,8 @@ var BUILT_IN_TASK_TYPES = {
4810
4823
  inputSchema: JudgeEvalVariantInput,
4811
4824
  outputSchema: JudgeEvalVariantOutput,
4812
4825
  outputKind: "judgment",
4826
+ workspaceScope: "attempt",
4827
+ sessionScope: "custom",
4813
4828
  requiresReferences: false,
4814
4829
  validateInput: validateJudgeEvalVariantInput,
4815
4830
  validateOutput: validateJudgeEvalVariantOutput,
@@ -5732,6 +5747,49 @@ function taskTypeUsesSubagents(taskType) {
5732
5747
  function taskTypeWorkspaceMode(taskType) {
5733
5748
  return getTaskTypeEntry(taskType)?.workspaceMode ?? "shared_mount";
5734
5749
  }
5750
+ /**
5751
+ * Whether the daemon may keep a warm executor session alive after one
5752
+ * attempt completes and reuse it for later related tasks.
5753
+ *
5754
+ * Unknown task types default to `false`.
5755
+ */
5756
+ function taskTypeResumable(taskType) {
5757
+ return getTaskTypeEntry(taskType)?.resumable === true;
5758
+ }
5759
+ /**
5760
+ * Lifetime of the task type's workspace.
5761
+ *
5762
+ * Unknown task types and task types without an explicit policy default to the
5763
+ * disposable per-attempt behavior.
5764
+ */
5765
+ function taskTypeWorkspaceScope(taskType) {
5766
+ return getTaskTypeEntry(taskType)?.workspaceScope ?? "attempt";
5767
+ }
5768
+ /**
5769
+ * Granularity of daemon-local warm-session reuse.
5770
+ *
5771
+ * Unknown task types and task types without an explicit policy default to no
5772
+ * reuse.
5773
+ */
5774
+ function taskTypeSessionScope(taskType) {
5775
+ return getTaskTypeEntry(taskType)?.sessionScope ?? "none";
5776
+ }
5777
+ /**
5778
+ * Resolve the daemon-relevant execution policy declared by a task type.
5779
+ *
5780
+ * This is the single read path for "may this task reuse a warm session?" and
5781
+ * "does its workspace belong to the attempt or the session?" so daemon code
5782
+ * doesn't need to hand-roll defaults or peek at the raw registry shape.
5783
+ */
5784
+ function getTaskExecutionPolicy(taskType) {
5785
+ return {
5786
+ resumable: taskTypeResumable(taskType),
5787
+ workspaceMode: taskTypeWorkspaceMode(taskType),
5788
+ workspaceScope: taskTypeWorkspaceScope(taskType),
5789
+ sessionScope: taskTypeSessionScope(taskType),
5790
+ usesSubagents: taskTypeUsesSubagents(taskType)
5791
+ };
5792
+ }
5735
5793
  //#endregion
5736
5794
  //#region ../../libs/tasks/src/wire.ts
5737
5795
  /**
@@ -5994,6 +6052,10 @@ var COMMON_OPTIONAL_FLAGS = `\
5994
6052
  this, the pi session aborts and the attempt
5995
6053
  fails with code 'max_bash_timeouts_exceeded'.
5996
6054
  0 = disabled. Default: 3.
6055
+ --warm-session-ttl-sec <n> Retain resumable daemon slots (Pi sessions +
6056
+ reusable worktrees) in local daemon state for
6057
+ this many seconds after use. 0 = disable reuse.
6058
+ Default: 1800.
5997
6059
  --debug Verbose logging: also log successful list/claim
5998
6060
  outcomes (candidate counts, claim attempts).`;
5999
6061
  var REGISTERED_TASK_TYPES = Object.keys(BUILT_IN_TASK_TYPES).sort();
@@ -6106,78 +6168,6 @@ function isHelpFlag(args) {
6106
6168
  return args.includes("--help") || args.includes("-h");
6107
6169
  }
6108
6170
  //#endregion
6109
- //#region ../../libs/agent-runtime/src/subagent-output-contracts.ts
6110
- var REGISTRY = /* @__PURE__ */ new Map();
6111
- /**
6112
- * Register a subagent output contract. Idempotent: re-registering the
6113
- * same name with a different schema throws — contracts are meant to
6114
- * be stable. Re-registering with the identical contract object (same
6115
- * reference) is a no-op for HMR and test convenience.
6116
- *
6117
- * Typically called at module-init time alongside task-type
6118
- * registration. See task-types/index.ts in @moltnet/tasks for the
6119
- * conventional pattern.
6120
- */
6121
- function registerSubagentOutputContract(contract) {
6122
- if (!contract.name || contract.name.trim().length === 0) throw new Error("subagent output contract name is required");
6123
- if (!/^[a-z][a-z0-9_]*$/.test(contract.name)) throw new Error(`subagent output contract name '${contract.name}' must be lower_snake_case (starts with a letter, then [a-z0-9_]+)`);
6124
- const existing = REGISTRY.get(contract.name);
6125
- if (existing && existing !== contract) {
6126
- if (existing.parametersSchema !== contract.parametersSchema) throw new Error(`subagent output contract '${contract.name}' is already registered with a different schema; refusing to override`);
6127
- }
6128
- REGISTRY.set(contract.name, contract);
6129
- }
6130
- /**
6131
- * Resolve a subagent output contract by name. Returns `null` for
6132
- * unknown names — callers (the subagent custom tool) decide whether
6133
- * that's a tool error the parent LLM can recover from or a hard fail.
6134
- */
6135
- function getSubagentOutputContract(name) {
6136
- return REGISTRY.get(name) ?? null;
6137
- }
6138
- /**
6139
- * List all registered contracts. Useful for diagnostics and for the
6140
- * subagent tool's parameter description so a parent LLM can see what
6141
- * contracts are available without enumerating them in its prompt.
6142
- */
6143
- function listSubagentOutputContracts() {
6144
- return [...REGISTRY.values()];
6145
- }
6146
- //#endregion
6147
- //#region ../../libs/agent-runtime/src/built-in-contract-registrations.ts
6148
- /**
6149
- * Built-in subagent output contracts (#1087, #943).
6150
- *
6151
- * Why this is an exported function and not a module-init side
6152
- * effect:
6153
- *
6154
- * - The registry is process-global. Module-init registration
6155
- * fires exactly once per Node process (ESM modules are cached
6156
- * by URL). Tests that call `__resetSubagentOutputContractsForTests()`
6157
- * to start from an empty registry have no way to repopulate
6158
- * the built-ins without re-evaluating the module — which the
6159
- * cache prevents. PR #1101 review M4.
6160
- * - An explicit `registerBuiltInSubagentContracts()` lets the
6161
- * package index call it once at module load AND lets test
6162
- * setup hooks call it again after `__reset...`.
6163
- * - `registerSubagentOutputContract` is itself idempotent for
6164
- * identical re-registrations, so calling this function twice
6165
- * in the same process is safe.
6166
- *
6167
- * Adding a new built-in: extend the body of this function. Do not
6168
- * call `registerSubagentOutputContract` from anywhere else in the
6169
- * package — keeping all built-ins in one function makes the set
6170
- * auditable.
6171
- */
6172
- function registerBuiltInSubagentContracts() {
6173
- registerSubagentOutputContract({
6174
- name: "judge_eval_variant_result",
6175
- description: "Per-variant grading result produced by a subagent of judge_eval_variant: scores against the shared rubric, composite, and a 1-3 sentence verdict for a single variant.",
6176
- parametersSchema: JudgeEvalVariantResult
6177
- });
6178
- }
6179
- registerBuiltInSubagentContracts();
6180
- //#endregion
6181
6171
  //#region ../../libs/agent-runtime/src/context-bindings.ts
6182
6172
  var PROMPT_SEPARATOR = "\n\n---\n\n";
6183
6173
  /**
@@ -6637,13 +6627,7 @@ function buildCuratePackUserPrompt(input, ctx) {
6637
6627
  * is told to inspect them itself.
6638
6628
  */
6639
6629
  function buildFulfillBriefUserPrompt(input, ctx) {
6640
- const { brief, title, acceptanceCriteria, seedFiles, scopeHint } = input;
6641
- const criteriaSection = acceptanceCriteria?.length ? [
6642
- "### Acceptance criteria",
6643
- "",
6644
- ...acceptanceCriteria.map((c) => `- ${c}`),
6645
- ""
6646
- ].join("\n") : "";
6630
+ const { brief, title, seedFiles, scopeHint } = input;
6647
6631
  const seedSection = seedFiles?.length ? [
6648
6632
  "### Seed files",
6649
6633
  "",
@@ -6691,7 +6675,6 @@ function buildFulfillBriefUserPrompt(input, ctx) {
6691
6675
  "",
6692
6676
  brief,
6693
6677
  "",
6694
- criteriaSection,
6695
6678
  seedSection,
6696
6679
  correlationSection,
6697
6680
  workspaceSection,
@@ -13853,6 +13836,31 @@ function abortableSleep(ms, signal) {
13853
13836
  });
13854
13837
  }
13855
13838
  //#endregion
13839
+ //#region ../../libs/agent-runtime/src/subagent-output-contracts.ts
13840
+ /**
13841
+ * Construct an immutable contract registry from a static list.
13842
+ *
13843
+ * The resulting registry is safe to share across sessions and
13844
+ * invocations — no mutation is possible after construction.
13845
+ */
13846
+ function createSubagentContractRegistry(contracts) {
13847
+ const lookup = /* @__PURE__ */ new Map();
13848
+ for (const c of contracts) {
13849
+ if (!c.name || c.name.trim().length === 0) throw new Error("subagent output contract name is required");
13850
+ if (!/^[a-z][a-z0-9_]*$/.test(c.name)) throw new Error(`subagent output contract name '${c.name}' must be lower_snake_case (starts with a letter, then [a-z0-9_]+)`);
13851
+ if (lookup.has(c.name)) throw new Error(`duplicate subagent output contract name '${c.name}' in constructor args`);
13852
+ lookup.set(c.name, c);
13853
+ }
13854
+ return {
13855
+ get(name) {
13856
+ return lookup.get(name) ?? null;
13857
+ },
13858
+ list() {
13859
+ return [...lookup.values()];
13860
+ }
13861
+ };
13862
+ }
13863
+ //#endregion
13856
13864
  //#region ../../libs/pi-extension/src/moltnet/render-phase6.ts
13857
13865
  function slugToTitle(value) {
13858
13866
  return value.split(/[:/_-]+/).filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" ");
@@ -14829,7 +14837,8 @@ function findMainWorktree() {
14829
14837
  function loadCredentials(agentDir) {
14830
14838
  const moltnetJson = readFileSync(path.join(agentDir, "moltnet.json"), "utf8");
14831
14839
  const agentEnvRaw = readFileSync(path.join(agentDir, "env"), "utf8");
14832
- const piAuthPath = process.env.PI_AUTH_PATH ?? path.join(process.env.HOME ?? "", ".pi", "agent", "auth.json");
14840
+ const piAgentDir = process.env.PI_CODING_AGENT_DIR ?? path.join(process.env.HOME ?? "", ".pi", "agent");
14841
+ const piAuthPath = path.join(piAgentDir, "auth.json");
14833
14842
  const piAuthJson = existsSync(piAuthPath) ? readFileSync(piAuthPath, "utf8") : null;
14834
14843
  const gitconfigPath = path.join(agentDir, "gitconfig");
14835
14844
  const gitconfig = existsSync(gitconfigPath) ? readFileSync(gitconfigPath, "utf8") : null;
@@ -15329,9 +15338,11 @@ var NO_SKILLS = () => ({
15329
15338
  diagnostics: []
15330
15339
  });
15331
15340
  /**
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.
15341
+ * Construct an `AgentSession`. By default it is in-memory; callers may opt
15342
+ * parent sessions into daemon-owned file persistence via `sessionPersistence`.
15343
+ * The caller is responsible for eventually invoking `session.prompt(...)` and
15344
+ * for tearing down — the helper does no lifecycle management beyond
15345
+ * construction.
15335
15346
  */
15336
15347
  async function buildAgentSession(args) {
15337
15348
  const piOtelExtension = createPiOtelExtension({
@@ -15339,22 +15350,30 @@ async function buildAgentSession(args) {
15339
15350
  spanAttributes: args.otelSpanAttrs
15340
15351
  });
15341
15352
  const resourceLoader = new DefaultResourceLoader({
15342
- cwd: args.mountPath,
15353
+ cwd: args.cwdPath,
15343
15354
  agentDir: args.piAuthDir,
15344
15355
  extensionFactories: [piOtelExtension],
15345
15356
  appendSystemPrompt: args.appendSystemPrompt,
15346
15357
  skillsOverride: args.skillsOverride ?? NO_SKILLS
15347
15358
  });
15348
15359
  await resourceLoader.reload();
15360
+ const sessionManager = args.sessionPersistence ? await resolvePersistentSessionManager({
15361
+ cwd: args.cwdPath,
15362
+ sessionDir: args.sessionPersistence.sessionDir
15363
+ }) : SessionManager.inMemory(args.cwdPath);
15349
15364
  return (await createAgentSession({
15350
15365
  agentDir: args.piAuthDir,
15351
- cwd: args.mountPath,
15366
+ cwd: args.cwdPath,
15352
15367
  model: args.modelHandle,
15353
15368
  customTools: args.customTools,
15354
- sessionManager: SessionManager.inMemory(),
15369
+ sessionManager,
15355
15370
  resourceLoader
15356
15371
  })).session;
15357
15372
  }
15373
+ async function resolvePersistentSessionManager(args) {
15374
+ await SessionManager.list(args.cwd, args.sessionDir);
15375
+ return SessionManager.continueRecent(args.cwd, args.sessionDir);
15376
+ }
15358
15377
  //#endregion
15359
15378
  //#region ../../libs/pi-extension/src/runtime/inject-task-context.ts
15360
15379
  /**
@@ -15589,6 +15608,7 @@ var DEFAULT_SUBAGENT_TIMEOUT_MS = 300 * 1e3;
15589
15608
  */
15590
15609
  function createSubagentTool(args) {
15591
15610
  const buildSession = args.buildAgentSession ?? buildAgentSession;
15611
+ const { contractRegistry } = args;
15592
15612
  let callCount = 0;
15593
15613
  return {
15594
15614
  tool: defineTool({
@@ -15599,8 +15619,8 @@ function createSubagentTool(args) {
15599
15619
  async execute(_id, params) {
15600
15620
  if (!Check(SubagentToolParameters, params)) return toolError(`subagent: invalid parameters: ${JSON.stringify([...Errors(SubagentToolParameters, params)].slice(0, 3))}`);
15601
15621
  const { task, output_schema } = params;
15602
- const contract = getSubagentOutputContract(output_schema);
15603
- if (!contract) return toolError(`subagent: unknown output_schema "${output_schema}". Registered contracts: [${listSubagentOutputContracts().map((c) => c.name).join(", ")}]`);
15622
+ const contract = contractRegistry.get(output_schema);
15623
+ if (!contract) return toolError(`subagent: unknown output_schema "${output_schema}". Registered contracts: [${contractRegistry.list().map((c) => c.name).join(", ")}]`);
15604
15624
  callCount += 1;
15605
15625
  const callIndex = callCount;
15606
15626
  let captured = null;
@@ -15630,6 +15650,7 @@ function createSubagentTool(args) {
15630
15650
  });
15631
15651
  const session = await buildSession({
15632
15652
  mountPath: args.mountPath,
15653
+ cwdPath: args.cwdPath ?? args.mountPath,
15633
15654
  piAuthDir: args.piAuthDir,
15634
15655
  modelHandle: args.modelHandle,
15635
15656
  agentName: args.agentName,
@@ -15965,6 +15986,115 @@ function resolveSubmitTools(taskType, opts = {}) {
15965
15986
  };
15966
15987
  }
15967
15988
  //#endregion
15989
+ //#region ../../libs/pi-extension/src/runtime/task-workspace.ts
15990
+ function prepareTaskWorkspace(task, requestedMountPath, executionPlan) {
15991
+ const branch = executionPlan?.worktreeBranch ?? null;
15992
+ if (!branch) return {
15993
+ mountPath: requestedMountPath,
15994
+ cwdPath: requestedMountPath,
15995
+ mode: "shared_mount",
15996
+ branch: null,
15997
+ cleanup: () => {}
15998
+ };
15999
+ const mainRepo = findMainWorktree();
16000
+ const worktreeDir = resolveTaskWorktreePath(mainRepo, executionPlan?.workspaceId ?? `task-${task.id}`);
16001
+ const relMount = relative(mainRepo, requestedMountPath);
16002
+ const cwdPath = relMount === "" || relMount.startsWith("..") ? worktreeDir : join(worktreeDir, relMount);
16003
+ const keepWorkspace = executionPlan?.workspaceScope === "session" && executionPlan.sessionKey !== null;
16004
+ if (keepWorkspace) ensureReusableTaskWorktree(mainRepo, worktreeDir, branch);
16005
+ else {
16006
+ removeExistingTaskWorktree(mainRepo, worktreeDir);
16007
+ addTaskWorktree(mainRepo, worktreeDir, branch);
16008
+ }
16009
+ return {
16010
+ mountPath: mainRepo,
16011
+ cwdPath,
16012
+ mode: "dedicated_worktree",
16013
+ branch,
16014
+ cleanup: keepWorkspace ? () => {} : () => {
16015
+ execFileSync("git", [
16016
+ "-C",
16017
+ mainRepo,
16018
+ "worktree",
16019
+ "remove",
16020
+ "--force",
16021
+ worktreeDir
16022
+ ], { stdio: "pipe" });
16023
+ }
16024
+ };
16025
+ }
16026
+ function resolveTaskWorktreePath(mainRepo, workspaceId) {
16027
+ return join(mainRepo, ".worktrees", workspaceId);
16028
+ }
16029
+ function ensureReusableTaskWorktree(mainRepo, worktreeDir, branch) {
16030
+ if (isRegisteredWorktree$1(mainRepo, worktreeDir)) return;
16031
+ if (existsSync(worktreeDir)) throw new Error(`Expected reusable worktree ${worktreeDir} to be git-managed, but it exists outside git worktree metadata.`);
16032
+ addTaskWorktree(mainRepo, worktreeDir, branch);
16033
+ }
16034
+ function addTaskWorktree(mainRepo, worktreeDir, branch) {
16035
+ const baseRef = resolveWorktreeBaseRef(mainRepo);
16036
+ execFileSync("git", gitRefExists(mainRepo, `refs/heads/${branch}`) ? [
16037
+ "-C",
16038
+ mainRepo,
16039
+ "worktree",
16040
+ "add",
16041
+ worktreeDir,
16042
+ branch
16043
+ ] : [
16044
+ "-C",
16045
+ mainRepo,
16046
+ "worktree",
16047
+ "add",
16048
+ "-b",
16049
+ branch,
16050
+ worktreeDir,
16051
+ baseRef
16052
+ ], { stdio: "pipe" });
16053
+ }
16054
+ function removeExistingTaskWorktree(mainRepo, worktreeDir) {
16055
+ if (!existsSync(worktreeDir) || !isRegisteredWorktree$1(mainRepo, worktreeDir)) return;
16056
+ execFileSync("git", [
16057
+ "-C",
16058
+ mainRepo,
16059
+ "worktree",
16060
+ "remove",
16061
+ "--force",
16062
+ worktreeDir
16063
+ ], { stdio: "pipe" });
16064
+ }
16065
+ function isRegisteredWorktree$1(mainRepo, worktreeDir) {
16066
+ const list = execFileSync("git", [
16067
+ "-C",
16068
+ mainRepo,
16069
+ "worktree",
16070
+ "list",
16071
+ "--porcelain"
16072
+ ], {
16073
+ encoding: "utf8",
16074
+ stdio: "pipe"
16075
+ });
16076
+ const marker = `worktree ${worktreeDir}\n`;
16077
+ return list.includes(marker) || list.endsWith(`worktree ${worktreeDir}`);
16078
+ }
16079
+ function resolveWorktreeBaseRef(mainRepo) {
16080
+ return gitRefExists(mainRepo, "refs/heads/main") ? "main" : "HEAD";
16081
+ }
16082
+ function gitRefExists(mainRepo, ref) {
16083
+ try {
16084
+ execFileSync("git", [
16085
+ "-C",
16086
+ mainRepo,
16087
+ "show-ref",
16088
+ "--verify",
16089
+ "--quiet",
16090
+ ref
16091
+ ], { stdio: "pipe" });
16092
+ return true;
16093
+ } catch {
16094
+ return false;
16095
+ }
16096
+ }
16097
+ //#endregion
15968
16098
  //#region ../../libs/pi-extension/src/runtime/execute-pi-task.ts
15969
16099
  /**
15970
16100
  * executePiTask — run a single Task attempt using pi-coding-agent inside a
@@ -15991,15 +16121,24 @@ var noopTurnEventHandler = () => {};
15991
16121
  function createPiTaskExecutor(opts) {
15992
16122
  let cachedCheckpoint = opts.checkpointPath ?? null;
15993
16123
  return async (claimedTask, reporter) => {
15994
- if (!cachedCheckpoint) cachedCheckpoint = await ensureSnapshot({
15995
- config: opts.sandboxConfig?.snapshot,
15996
- onProgress: opts.onSnapshotProgress ?? ((m) => {
15997
- process.stderr.write(`[snapshot] ${m}\n`);
15998
- })
16124
+ const reporterWasOpened = !reporter.cancelSignal.aborted;
16125
+ if (reporterWasOpened) await reporter.open({
16126
+ taskId: claimedTask.task.id,
16127
+ attemptN: claimedTask.attemptN
15999
16128
  });
16000
16129
  return executePiTask(claimedTask, reporter, {
16001
16130
  ...opts,
16002
- checkpointPath: cachedCheckpoint
16131
+ checkpointPath: cachedCheckpoint ?? void 0,
16132
+ resolveCheckpointPath: async () => {
16133
+ if (!cachedCheckpoint) cachedCheckpoint = await ensureSnapshot({
16134
+ config: opts.sandboxConfig?.snapshot,
16135
+ onProgress: opts.onSnapshotProgress ?? ((m) => {
16136
+ process.stderr.write(`[snapshot] ${m}\n`);
16137
+ })
16138
+ });
16139
+ return cachedCheckpoint;
16140
+ },
16141
+ reporterAlreadyOpened: reporterWasOpened
16003
16142
  });
16004
16143
  };
16005
16144
  }
@@ -16013,8 +16152,11 @@ async function executePiTask(claimedTask, reporter, opts) {
16013
16152
  const task = claimedTask.task;
16014
16153
  const attemptN = claimedTask.attemptN;
16015
16154
  const startTime = Date.now();
16016
- const workspace = prepareTaskWorkspace(task, opts.mountPath ?? process.cwd());
16017
- const mountPath = workspace.mountPath;
16155
+ const requestedMountPath = opts.mountPath ?? process.cwd();
16156
+ const executionPlan = opts.makeExecutionPlan?.(claimedTask) ?? null;
16157
+ let workspace = null;
16158
+ let mountPath = requestedMountPath;
16159
+ let cwdPath = requestedMountPath;
16018
16160
  if (reporter.cancelSignal.aborted) return {
16019
16161
  taskId: task.id,
16020
16162
  attemptN,
@@ -16029,33 +16171,8 @@ async function executePiTask(claimedTask, reporter, opts) {
16029
16171
  retryable: false
16030
16172
  }
16031
16173
  };
16032
- const checkpointPath = opts.checkpointPath ?? await ensureSnapshot({
16033
- config: opts.sandboxConfig?.snapshot,
16034
- onProgress: opts.onSnapshotProgress ?? ((m) => {
16035
- process.stderr.write(`[snapshot] ${m}\n`);
16036
- })
16037
- });
16038
- const mainRepoForRepair = findMainWorktree();
16039
- try {
16040
- execFileSync("git", [
16041
- "-C",
16042
- mainRepoForRepair,
16043
- "worktree",
16044
- "repair",
16045
- "--relative-paths"
16046
- ], { stdio: "pipe" });
16047
- } catch {}
16174
+ let reporterOpen = opts.reporterAlreadyOpened ?? false;
16048
16175
  let managed = null;
16049
- managed = await resumeVm({
16050
- checkpointPath,
16051
- agentName: opts.agentName,
16052
- mountPath,
16053
- extraAllowedHosts: opts.extraAllowedHosts,
16054
- sandboxConfig: opts.sandboxConfig
16055
- });
16056
- const diaryId = task.diaryId ?? "";
16057
- const taskTeamId = task.teamId ?? "";
16058
- let reporterOpen = false;
16059
16176
  let session = null;
16060
16177
  let subagentHandle = null;
16061
16178
  const finalUsage = emptyUsage(opts.provider, opts.model);
@@ -16074,41 +16191,103 @@ async function executePiTask(claimedTask, reporter, opts) {
16074
16191
  retryable: false
16075
16192
  }
16076
16193
  });
16194
+ let onTurnEvent;
16195
+ if (opts.makeOnTurnEvent) try {
16196
+ onTurnEvent = opts.makeOnTurnEvent(claimedTask);
16197
+ } catch (err) {
16198
+ process.stderr.write(`[emit] makeOnTurnEvent threw: ${err instanceof Error ? err.message : String(err)}\n`);
16199
+ onTurnEvent = noopTurnEventHandler;
16200
+ }
16201
+ else onTurnEvent = opts.onTurnEvent ?? noopTurnEventHandler;
16202
+ const emit = (kind, payload) => {
16203
+ try {
16204
+ onTurnEvent(kind, summarizePayloadForLog(kind, payload));
16205
+ } catch (err) {
16206
+ process.stderr.write(`[emit] onTurnEvent threw for kind="${kind}": ${err instanceof Error ? err.message : String(err)}\n`);
16207
+ }
16208
+ return reporter.record({
16209
+ kind,
16210
+ payload
16211
+ });
16212
+ };
16213
+ const emitError = async (phase, message, extra = {}) => {
16214
+ await emit("error", {
16215
+ phase,
16216
+ message,
16217
+ ...extra
16218
+ });
16219
+ };
16077
16220
  try {
16078
- const mainRepo = findMainWorktree();
16079
- activateAgentEnv(managed.credentials.agentEnv, mainRepo);
16080
- await reporter.open({
16221
+ if (!opts.reporterAlreadyOpened) await reporter.open({
16081
16222
  taskId: task.id,
16082
16223
  attemptN
16083
16224
  });
16084
16225
  reporterOpen = true;
16085
- let onTurnEvent;
16086
- if (opts.makeOnTurnEvent) try {
16087
- onTurnEvent = opts.makeOnTurnEvent(claimedTask);
16226
+ let checkpointPath;
16227
+ try {
16228
+ checkpointPath = opts.checkpointPath ?? (opts.resolveCheckpointPath ? await opts.resolveCheckpointPath() : await ensureSnapshot({
16229
+ config: opts.sandboxConfig?.snapshot,
16230
+ onProgress: opts.onSnapshotProgress ?? ((m) => {
16231
+ process.stderr.write(`[snapshot] ${m}\n`);
16232
+ })
16233
+ }));
16088
16234
  } catch (err) {
16089
- process.stderr.write(`[emit] makeOnTurnEvent threw: ${err instanceof Error ? err.message : String(err)}\n`);
16090
- onTurnEvent = noopTurnEventHandler;
16235
+ const message = err instanceof Error ? err.message : String(err);
16236
+ await emitError("snapshot", message);
16237
+ return makeFailedOutput("snapshot_failed", message);
16091
16238
  }
16092
- else onTurnEvent = opts.onTurnEvent ?? noopTurnEventHandler;
16093
- const emit = (kind, payload) => {
16239
+ try {
16240
+ workspace = prepareTaskWorkspace(task, requestedMountPath, executionPlan);
16241
+ mountPath = workspace.mountPath;
16242
+ cwdPath = workspace.cwdPath;
16243
+ } catch (err) {
16244
+ const message = err instanceof Error ? err.message : String(err);
16245
+ await emitError("worktree_setup", message);
16246
+ return makeFailedOutput("worktree_setup_failed", message);
16247
+ }
16248
+ try {
16249
+ const mainRepoForRepair = findMainWorktree();
16094
16250
  try {
16095
- onTurnEvent(kind, summarizePayloadForLog(kind, payload));
16096
- } catch (err) {
16097
- process.stderr.write(`[emit] onTurnEvent threw for kind="${kind}": ${err instanceof Error ? err.message : String(err)}\n`);
16098
- }
16099
- return reporter.record({
16100
- kind,
16101
- payload
16251
+ execFileSync("git", [
16252
+ "-C",
16253
+ mainRepoForRepair,
16254
+ "worktree",
16255
+ "repair",
16256
+ "--relative-paths"
16257
+ ], { stdio: "pipe" });
16258
+ } catch {}
16259
+ } catch (err) {
16260
+ const message = err instanceof Error ? err.message : String(err);
16261
+ await emitError("worktree_setup", message);
16262
+ return makeFailedOutput("worktree_setup_failed", message);
16263
+ }
16264
+ try {
16265
+ managed = await resumeVm({
16266
+ checkpointPath,
16267
+ agentName: opts.agentName,
16268
+ mountPath,
16269
+ extraAllowedHosts: opts.extraAllowedHosts,
16270
+ sandboxConfig: opts.sandboxConfig
16102
16271
  });
16103
- };
16272
+ } catch (err) {
16273
+ const message = err instanceof Error ? err.message : String(err);
16274
+ await emitError("vm_resume", message);
16275
+ return makeFailedOutput("vm_resume_failed", message);
16276
+ }
16277
+ const diaryId = task.diaryId ?? "";
16278
+ const taskTeamId = task.teamId ?? "";
16279
+ const mainRepo = findMainWorktree();
16280
+ activateAgentEnv(managed.credentials.agentEnv, mainRepo);
16281
+ const activeWorkspace = workspace;
16282
+ if (!activeWorkspace) throw new Error("task workspace not prepared");
16104
16283
  await emit("info", {
16105
16284
  event: "execute_start",
16106
16285
  taskType: task.taskType,
16107
16286
  teamId: task.teamId,
16108
16287
  provider: opts.provider,
16109
16288
  model: opts.model,
16110
- workspaceMode: workspace.mode,
16111
- workspaceBranch: workspace.branch
16289
+ workspaceMode: activeWorkspace.mode,
16290
+ workspaceBranch: activeWorkspace.branch
16112
16291
  });
16113
16292
  let taskPrompt;
16114
16293
  try {
@@ -16116,8 +16295,8 @@ async function executePiTask(claimedTask, reporter, opts) {
16116
16295
  diaryId,
16117
16296
  taskId: task.id,
16118
16297
  workspace: {
16119
- mode: workspace.mode,
16120
- branch: workspace.branch
16298
+ mode: activeWorkspace.mode,
16299
+ branch: activeWorkspace.branch
16121
16300
  },
16122
16301
  extras: opts.promptExtras
16123
16302
  });
@@ -16169,7 +16348,7 @@ async function executePiTask(claimedTask, reporter, opts) {
16169
16348
  getTeamId: () => taskTeamId,
16170
16349
  getSessionErrors: () => [],
16171
16350
  clearSessionErrors: () => {},
16172
- getHostCwd: () => mountPath,
16351
+ getHostCwd: () => cwdPath,
16173
16352
  hostExecBaseEnv: new Set([...HOST_EXEC_DEFAULT_BASE_ENV, ...Object.keys(managed.credentials.agentEnv)]),
16174
16353
  hostExecAutoApprove: opts.hostExecAutoApprove ?? opts.sandboxConfig?.hostExec?.autoApprove ?? false,
16175
16354
  getTaskContext: () => ({
@@ -16197,6 +16376,7 @@ async function executePiTask(claimedTask, reporter, opts) {
16197
16376
  if (taskTypeUsesSubagents(task.taskType)) {
16198
16377
  subagentHandle = createSubagentTool({
16199
16378
  mountPath,
16379
+ cwdPath,
16200
16380
  piAuthDir,
16201
16381
  modelHandle,
16202
16382
  agentName: opts.agentName,
@@ -16205,12 +16385,14 @@ async function executePiTask(claimedTask, reporter, opts) {
16205
16385
  parentTaskId: task.id,
16206
16386
  parentTaskType: task.taskType,
16207
16387
  parentAttemptN: attemptN,
16388
+ contractRegistry: opts.subagentContractRegistry,
16208
16389
  parentCancelSignal: reporter.cancelSignal
16209
16390
  });
16210
16391
  parentSubagentTools.push(subagentHandle.tool);
16211
16392
  }
16212
16393
  session = await buildAgentSession({
16213
16394
  mountPath,
16395
+ cwdPath,
16214
16396
  piAuthDir,
16215
16397
  modelHandle,
16216
16398
  agentName: opts.agentName,
@@ -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);
@@ -16294,6 +16477,10 @@ async function executePiTask(claimedTask, reporter, opts) {
16294
16477
  is_error: event.isError,
16295
16478
  result: event.isError ? truncateForWire(event.result) : void 0
16296
16479
  }));
16480
+ if (event.isError) track(emitError("tool_call_error", describeToolErrorMessage(event.result), {
16481
+ tool: event.toolName,
16482
+ result: truncateForWire(event.result)
16483
+ }));
16297
16484
  if (maxBashTimeouts > 0 && event.toolName === "bash" && event.isError && isBashTimeoutResult(event.result)) {
16298
16485
  bashTimeoutCount += 1;
16299
16486
  if (bashTimeoutCount >= maxBashTimeouts) triggerCapAbort("max_bash_timeouts_exceeded", `Aborted after ${bashTimeoutCount} bash timeouts in this attempt (cap ${maxBashTimeouts}).`);
@@ -16449,7 +16636,7 @@ async function executePiTask(claimedTask, reporter, opts) {
16449
16636
  }
16450
16637
  }
16451
16638
  if (managed) await managed.vm.close();
16452
- try {
16639
+ if (workspace) try {
16453
16640
  workspace.cleanup();
16454
16641
  } catch (err) {
16455
16642
  const detail = err instanceof Error ? err.message : String(err);
@@ -16457,107 +16644,6 @@ async function executePiTask(claimedTask, reporter, opts) {
16457
16644
  }
16458
16645
  }
16459
16646
  }
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
16647
  function emptyUsage(provider, model) {
16562
16648
  return {
16563
16649
  inputTokens: 0,
@@ -16661,6 +16747,23 @@ function truncateForWire(value) {
16661
16747
  };
16662
16748
  }
16663
16749
  }
16750
+ function describeToolErrorMessage(result) {
16751
+ if (typeof result === "string" && result.trim().length > 0) return result.trim();
16752
+ if (result && typeof result === "object") {
16753
+ const content = result.content;
16754
+ if (Array.isArray(content)) {
16755
+ for (const item of content) if (item && typeof item === "object" && typeof item.text === "string") {
16756
+ const text = item.text.trim();
16757
+ if (text.length > 0) return text;
16758
+ }
16759
+ }
16760
+ }
16761
+ try {
16762
+ return JSON.stringify(truncateForWire(result));
16763
+ } catch {
16764
+ return "Tool call failed";
16765
+ }
16766
+ }
16664
16767
  //#endregion
16665
16768
  //#region src/config.ts
16666
16769
  function loadConfig() {
@@ -16757,6 +16860,419 @@ function createGhCliClient() {
16757
16860
  };
16758
16861
  }
16759
16862
  //#endregion
16863
+ //#region src/lib/daemon-slot-registry.ts
16864
+ var DaemonSlotRegistryError = class extends Error {
16865
+ constructor(operation, cause) {
16866
+ super(`Daemon slot registry failed during ${operation}`, { cause });
16867
+ this.operation = operation;
16868
+ this.name = "DaemonSlotRegistryError";
16869
+ }
16870
+ };
16871
+ var DaemonSlotRegistry = class {
16872
+ db;
16873
+ constructor(dbPath) {
16874
+ try {
16875
+ this.db = new DatabaseSync(dbPath);
16876
+ this.withDb("initialize schema", () => {
16877
+ this.db.exec(`
16878
+ PRAGMA journal_mode = WAL;
16879
+
16880
+ CREATE TABLE IF NOT EXISTS daemon_slots (
16881
+ agent_name TEXT NOT NULL,
16882
+ provider TEXT NOT NULL,
16883
+ model TEXT NOT NULL,
16884
+ slot_key TEXT NOT NULL,
16885
+ task_type TEXT NOT NULL,
16886
+ state TEXT NOT NULL CHECK (state IN ('active', 'idle')),
16887
+ last_task_id TEXT NOT NULL,
16888
+ last_attempt_n INTEGER NOT NULL,
16889
+ created_at_ms INTEGER NOT NULL,
16890
+ last_used_at_ms INTEGER NOT NULL,
16891
+ expires_at_ms INTEGER NOT NULL,
16892
+ PRIMARY KEY (agent_name, provider, model, slot_key)
16893
+ );
16894
+
16895
+ CREATE TABLE IF NOT EXISTS daemon_slot_sessions (
16896
+ agent_name TEXT NOT NULL,
16897
+ provider TEXT NOT NULL,
16898
+ model TEXT NOT NULL,
16899
+ slot_key TEXT NOT NULL,
16900
+ session_dir TEXT NOT NULL UNIQUE,
16901
+ session_path TEXT,
16902
+ PRIMARY KEY (agent_name, provider, model, slot_key),
16903
+ FOREIGN KEY (agent_name, provider, model, slot_key)
16904
+ REFERENCES daemon_slots(agent_name, provider, model, slot_key)
16905
+ ON DELETE CASCADE
16906
+ );
16907
+
16908
+ CREATE TABLE IF NOT EXISTS daemon_slot_workspaces (
16909
+ agent_name TEXT NOT NULL,
16910
+ provider TEXT NOT NULL,
16911
+ model TEXT NOT NULL,
16912
+ slot_key TEXT NOT NULL,
16913
+ workspace_id TEXT NOT NULL UNIQUE,
16914
+ worktree_path TEXT NOT NULL,
16915
+ worktree_branch TEXT,
16916
+ PRIMARY KEY (agent_name, provider, model, slot_key),
16917
+ FOREIGN KEY (agent_name, provider, model, slot_key)
16918
+ REFERENCES daemon_slots(agent_name, provider, model, slot_key)
16919
+ ON DELETE CASCADE
16920
+ );
16921
+
16922
+ CREATE INDEX IF NOT EXISTS daemon_slots_expires_idx
16923
+ ON daemon_slots (expires_at_ms);
16924
+ `);
16925
+ });
16926
+ } catch (error) {
16927
+ throw new DaemonSlotRegistryError("open database", error);
16928
+ }
16929
+ }
16930
+ close() {
16931
+ try {
16932
+ this.db.close();
16933
+ } catch (error) {
16934
+ throw new DaemonSlotRegistryError("close database", error);
16935
+ }
16936
+ }
16937
+ beginSlot(input) {
16938
+ const now = Date.now();
16939
+ const expiresAtMs = now + input.ttlSec * 1e3;
16940
+ this.withDb("upsert slot", () => this.db.prepare(`INSERT INTO daemon_slots (
16941
+ agent_name, provider, model, slot_key, task_type, state,
16942
+ last_task_id, last_attempt_n, created_at_ms, last_used_at_ms,
16943
+ expires_at_ms
16944
+ ) VALUES (
16945
+ :agentName, :provider, :model, :slotKey, :taskType, 'active',
16946
+ :lastTaskId, :lastAttemptN, :createdAtMs, :lastUsedAtMs,
16947
+ :expiresAtMs
16948
+ )
16949
+ ON CONFLICT(agent_name, provider, model, slot_key) DO UPDATE SET
16950
+ task_type = excluded.task_type,
16951
+ state = 'active',
16952
+ last_task_id = excluded.last_task_id,
16953
+ last_attempt_n = excluded.last_attempt_n,
16954
+ last_used_at_ms = excluded.last_used_at_ms,
16955
+ expires_at_ms = excluded.expires_at_ms`).run({
16956
+ agentName: input.agentName,
16957
+ provider: input.provider,
16958
+ model: input.model,
16959
+ slotKey: input.slotKey,
16960
+ taskType: input.taskType,
16961
+ lastTaskId: input.lastTaskId,
16962
+ lastAttemptN: input.lastAttemptN,
16963
+ createdAtMs: now,
16964
+ lastUsedAtMs: now,
16965
+ expiresAtMs
16966
+ }));
16967
+ if (input.sessionDir) this.withDb("upsert slot session", () => this.db.prepare(`INSERT INTO daemon_slot_sessions (
16968
+ agent_name, provider, model, slot_key, session_dir, session_path
16969
+ ) VALUES (
16970
+ :agentName, :provider, :model, :slotKey, :sessionDir, :sessionPath
16971
+ )
16972
+ ON CONFLICT(agent_name, provider, model, slot_key) DO UPDATE SET
16973
+ session_dir = excluded.session_dir,
16974
+ session_path = excluded.session_path`).run({
16975
+ agentName: input.agentName,
16976
+ provider: input.provider,
16977
+ model: input.model,
16978
+ slotKey: input.slotKey,
16979
+ sessionDir: input.sessionDir,
16980
+ sessionPath: input.sessionPath
16981
+ }));
16982
+ if (input.workspaceId && input.worktreePath) this.withDb("upsert slot workspace", () => this.db.prepare(`INSERT INTO daemon_slot_workspaces (
16983
+ agent_name, provider, model, slot_key, workspace_id, worktree_path,
16984
+ worktree_branch
16985
+ ) VALUES (
16986
+ :agentName, :provider, :model, :slotKey, :workspaceId, :worktreePath,
16987
+ :worktreeBranch
16988
+ )
16989
+ ON CONFLICT(agent_name, provider, model, slot_key) DO UPDATE SET
16990
+ workspace_id = excluded.workspace_id,
16991
+ worktree_path = excluded.worktree_path,
16992
+ worktree_branch = excluded.worktree_branch`).run({
16993
+ agentName: input.agentName,
16994
+ provider: input.provider,
16995
+ model: input.model,
16996
+ slotKey: input.slotKey,
16997
+ workspaceId: input.workspaceId,
16998
+ worktreePath: input.worktreePath,
16999
+ worktreeBranch: input.worktreeBranch
17000
+ }));
17001
+ }
17002
+ finishSlot(identity, slotKey, ttlSec, sessionPath) {
17003
+ const now = Date.now();
17004
+ this.withDb("finish slot", () => this.db.prepare(`UPDATE daemon_slots
17005
+ SET state = 'idle',
17006
+ last_used_at_ms = ?,
17007
+ expires_at_ms = ?
17008
+ WHERE agent_name = ? AND provider = ? AND model = ? AND slot_key = ?`).run(now, now + ttlSec * 1e3, identity.agentName, identity.provider, identity.model, slotKey));
17009
+ if (sessionPath !== null) this.withDb("update slot session path", () => this.db.prepare(`UPDATE daemon_slot_sessions
17010
+ SET session_path = ?
17011
+ WHERE agent_name = ? AND provider = ? AND model = ? AND slot_key = ?`).run(sessionPath, identity.agentName, identity.provider, identity.model, slotKey));
17012
+ }
17013
+ reapExpiredSlots(now = Date.now()) {
17014
+ this.withDb("begin reap transaction", () => this.db.exec("BEGIN IMMEDIATE"));
17015
+ try {
17016
+ const slots = this.withDb("select expired slots", () => this.db.prepare(`SELECT
17017
+ agent_name as agentName,
17018
+ provider,
17019
+ model,
17020
+ slot_key as slotKey,
17021
+ task_type as taskType,
17022
+ state,
17023
+ last_task_id as lastTaskId,
17024
+ last_attempt_n as lastAttemptN,
17025
+ created_at_ms as createdAtMs,
17026
+ last_used_at_ms as lastUsedAtMs,
17027
+ expires_at_ms as expiresAtMs
17028
+ FROM daemon_slots
17029
+ WHERE expires_at_ms <= ?`).all(now));
17030
+ if (slots.length < 1) {
17031
+ this.withDb("commit empty reap transaction", () => this.db.exec("COMMIT"));
17032
+ return [];
17033
+ }
17034
+ const selectSession = this.withDb("prepare slot session lookup", () => this.db.prepare(`SELECT
17035
+ agent_name as agentName,
17036
+ provider,
17037
+ model,
17038
+ slot_key as slotKey,
17039
+ session_dir as sessionDir,
17040
+ session_path as sessionPath
17041
+ FROM daemon_slot_sessions
17042
+ WHERE agent_name = ? AND provider = ? AND model = ? AND slot_key = ?`));
17043
+ const selectWorkspace = this.withDb("prepare slot workspace lookup", () => this.db.prepare(`SELECT
17044
+ agent_name as agentName,
17045
+ provider,
17046
+ model,
17047
+ slot_key as slotKey,
17048
+ workspace_id as workspaceId,
17049
+ worktree_path as worktreePath,
17050
+ worktree_branch as worktreeBranch
17051
+ FROM daemon_slot_workspaces
17052
+ WHERE agent_name = ? AND provider = ? AND model = ? AND slot_key = ?`));
17053
+ 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 = ?"));
17054
+ const out = [];
17055
+ for (const slot of slots) {
17056
+ const session = this.withDb("select slot session", () => selectSession.get(slot.agentName, slot.provider, slot.model, slot.slotKey) ?? null);
17057
+ const workspace = this.withDb("select slot workspace", () => selectWorkspace.get(slot.agentName, slot.provider, slot.model, slot.slotKey) ?? null);
17058
+ out.push({
17059
+ slot,
17060
+ session,
17061
+ workspace
17062
+ });
17063
+ this.withDb("delete expired slot", () => deleteStmt.run(slot.agentName, slot.provider, slot.model, slot.slotKey));
17064
+ }
17065
+ this.withDb("commit reap transaction", () => this.db.exec("COMMIT"));
17066
+ for (const item of out) {
17067
+ if (item.session) cleanupPiSessionDir(item.session.sessionDir);
17068
+ if (item.workspace) cleanupReusableWorktree(item.workspace.worktreePath);
17069
+ }
17070
+ return out;
17071
+ } catch (error) {
17072
+ try {
17073
+ this.db.exec("ROLLBACK");
17074
+ } catch {}
17075
+ throw error instanceof DaemonSlotRegistryError ? error : new DaemonSlotRegistryError("reap expired slots", error);
17076
+ }
17077
+ }
17078
+ withDb(operation, fn) {
17079
+ try {
17080
+ return fn();
17081
+ } catch (error) {
17082
+ throw new DaemonSlotRegistryError(operation, error);
17083
+ }
17084
+ }
17085
+ };
17086
+ function resolveLatestPiSessionPath(sessionDir) {
17087
+ try {
17088
+ const latestEntry = readdirSync(sessionDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => entry.name).sort().at(-1);
17089
+ return latestEntry ? join(sessionDir, latestEntry) : null;
17090
+ } catch {
17091
+ return null;
17092
+ }
17093
+ }
17094
+ function cleanupPiSessionDir(sessionDir) {
17095
+ rmSync(sessionDir, {
17096
+ recursive: true,
17097
+ force: true
17098
+ });
17099
+ }
17100
+ function cleanupReusableWorktree(worktreePath) {
17101
+ const mainRepo = findMainWorktree();
17102
+ if (isRegisteredWorktree(mainRepo, worktreePath)) {
17103
+ execFileSync("git", [
17104
+ "-C",
17105
+ mainRepo,
17106
+ "worktree",
17107
+ "remove",
17108
+ "--force",
17109
+ worktreePath
17110
+ ], { stdio: "pipe" });
17111
+ return;
17112
+ }
17113
+ rmSync(worktreePath, {
17114
+ recursive: true,
17115
+ force: true
17116
+ });
17117
+ }
17118
+ function isRegisteredWorktree(mainRepo, worktreeDir) {
17119
+ const list = execFileSync("git", [
17120
+ "-C",
17121
+ mainRepo,
17122
+ "worktree",
17123
+ "list",
17124
+ "--porcelain"
17125
+ ], {
17126
+ encoding: "utf8",
17127
+ stdio: "pipe"
17128
+ });
17129
+ const marker = `worktree ${worktreeDir}\n`;
17130
+ return list.includes(marker) || list.endsWith(`worktree ${worktreeDir}`);
17131
+ }
17132
+ //#endregion
17133
+ //#region src/lib/slugify.ts
17134
+ function slugifyAsciiLower(input, maxLen, preserveChars = []) {
17135
+ const preserved = new Set(preserveChars.map((char) => char.toLowerCase()));
17136
+ let out = "";
17137
+ let pendingDash = false;
17138
+ for (const rawChar of input) {
17139
+ const char = rawChar.toLowerCase();
17140
+ const isAlphaNum = char >= "a" && char <= "z" || char >= "0" && char <= "9";
17141
+ const isPreserved = preserved.has(char);
17142
+ if (isAlphaNum || isPreserved) {
17143
+ if (pendingDash && out.length > 0 && out.length < maxLen) out += "-";
17144
+ pendingDash = false;
17145
+ if (out.length < maxLen) out += char;
17146
+ else break;
17147
+ continue;
17148
+ }
17149
+ pendingDash = out.length > 0;
17150
+ }
17151
+ return out;
17152
+ }
17153
+ //#endregion
17154
+ //#region src/lib/session-policy.ts
17155
+ /**
17156
+ * Resolve the daemon-facing execution policy for a task and, when the task
17157
+ * type permits warm reuse, compute the daemon-local `sessionKey`.
17158
+ *
17159
+ * `correlationId` remains the audit/query key; `sessionKey` is narrower and
17160
+ * may fold in task-type-specific discriminators so eval/review flows stay
17161
+ * isolated even inside one correlation group.
17162
+ */
17163
+ function deriveTaskSessionDescriptor(task) {
17164
+ const policy = getTaskExecutionPolicy(task.taskType);
17165
+ return {
17166
+ policy,
17167
+ sessionKey: buildTaskSessionKey(task, policy)
17168
+ };
17169
+ }
17170
+ function buildTaskSessionKey(task, policy) {
17171
+ if (!policy.resumable) return null;
17172
+ switch (policy.sessionScope) {
17173
+ case "none": return null;
17174
+ case "correlation": return task.correlationId ? `${task.taskType}:correlation:${task.correlationId}` : null;
17175
+ case "custom": return buildCustomSessionKey(task);
17176
+ }
17177
+ }
17178
+ function buildCustomSessionKey(task) {
17179
+ switch (task.taskType) {
17180
+ case "run_eval": {
17181
+ const variantLabel = typeof task.input.variantLabel === "string" ? task.input.variantLabel : null;
17182
+ if (!task.correlationId || !variantLabel) return null;
17183
+ return `run_eval:correlation:${task.correlationId}:variant:${slugifySessionComponent(variantLabel)}`;
17184
+ }
17185
+ case "judge_eval_variant": {
17186
+ const runTaskIds = Array.isArray(task.input.runTaskIds) ? task.input.runTaskIds.filter((value) => typeof value === "string") : [];
17187
+ if (runTaskIds.length < 1) return null;
17188
+ return `judge_eval_variant:run_tasks:${[...runTaskIds].sort().join(",")}`;
17189
+ }
17190
+ default: return null;
17191
+ }
17192
+ }
17193
+ function slugifySessionComponent(input) {
17194
+ return slugifyAsciiLower(input.trim(), 64);
17195
+ }
17196
+ //#endregion
17197
+ //#region src/lib/task-execution-plan.ts
17198
+ function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmSessionTtlSec) {
17199
+ const descriptor = deriveTaskSessionDescriptor(task);
17200
+ const slotKey = warmSessionTtlSec > 0 ? descriptor.sessionKey : null;
17201
+ const workspaceScope = slotKey !== null ? descriptor.policy.workspaceScope : "attempt";
17202
+ const slotId = slotKey ? buildDaemonSlotId(identity, slotKey) : null;
17203
+ const sessionDir = slotId ? `${stateDirs.piSessionsDir}/${encodeURIComponent(slotId)}` : null;
17204
+ const worktreeBranch = resolveTaskWorktreeBranch(task, descriptor.policy);
17205
+ const workspaceId = worktreeBranch !== null ? resolveTaskWorkspaceId(task, {
17206
+ sessionKey: slotId,
17207
+ workspaceScope,
17208
+ sessionPersistence: sessionDir ? { sessionDir } : null
17209
+ }) : null;
17210
+ return {
17211
+ descriptor,
17212
+ sessionKey: slotId,
17213
+ slotKey,
17214
+ slotId,
17215
+ workspaceScope,
17216
+ sessionPersistence: sessionDir ? { sessionDir } : null,
17217
+ workspaceId,
17218
+ worktreeBranch
17219
+ };
17220
+ }
17221
+ function buildDaemonSlotId(identity, slotKey) {
17222
+ return [
17223
+ "agent",
17224
+ slugSlotIdentityComponent(identity.agentName),
17225
+ "provider",
17226
+ slugSlotIdentityComponent(identity.provider),
17227
+ "model",
17228
+ slugSlotIdentityComponent(identity.model),
17229
+ "key",
17230
+ slotKey
17231
+ ].join(":");
17232
+ }
17233
+ function slugSlotIdentityComponent(input) {
17234
+ return slugifyAsciiLower(input.trim(), 64, [
17235
+ ".",
17236
+ "_",
17237
+ "-"
17238
+ ]);
17239
+ }
17240
+ function resolveTaskWorktreeBranch(task, policy) {
17241
+ if (policy.workspaceMode !== "dedicated_worktree") return null;
17242
+ if (task.taskType === "fulfill_brief") {
17243
+ const input = task.input;
17244
+ 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";
17245
+ if (task.correlationId) return `moltnet/${task.correlationId}/${slug}`;
17246
+ return `feat/${(typeof input.scopeHint === "string" && input.scopeHint.trim().length > 0 ? slugifyAsciiLower(input.scopeHint, 60) : "task") || "task"}-${slug}`;
17247
+ }
17248
+ return `task/${slugifyAsciiLower(task.taskType, 60) || "task"}-${task.id.slice(0, 8)}`;
17249
+ }
17250
+ function resolveTaskWorkspaceId(task, executionPlan) {
17251
+ if (executionPlan.workspaceScope === "session" && executionPlan.sessionKey !== null) return `session-${encodeURIComponent(executionPlan.sessionKey)}`;
17252
+ return `task-${task.id}`;
17253
+ }
17254
+ //#endregion
17255
+ //#region src/lib/execution-plan-cache.ts
17256
+ function createExecutionPlanCache(args) {
17257
+ const cache = /* @__PURE__ */ new Map();
17258
+ return {
17259
+ getOrCreate(claimedTask) {
17260
+ const key = buildClaimedTaskKey(claimedTask);
17261
+ const existing = cache.get(key);
17262
+ if (existing) return existing;
17263
+ const plan = buildDaemonTaskExecutionPlan(claimedTask.task, args.stateDirs, args.slotIdentity, args.warmSessionTtlSec);
17264
+ cache.set(key, plan);
17265
+ return plan;
17266
+ },
17267
+ delete(claimedTask) {
17268
+ cache.delete(buildClaimedTaskKey(claimedTask));
17269
+ }
17270
+ };
17271
+ }
17272
+ function buildClaimedTaskKey(task) {
17273
+ return `${task.task.id}:${task.attemptN}`;
17274
+ }
17275
+ //#endregion
16760
17276
  //#region src/lib/finalize.ts
16761
17277
  async function finalizeTask(agent, output, ctx = {}) {
16762
17278
  if (output.status === "cancelled") return;
@@ -16841,7 +17357,8 @@ var DEFAULTS = {
16841
17357
  maxBatchSize: 50,
16842
17358
  flushIntervalMs: 200,
16843
17359
  maxTurns: 0,
16844
- maxBashTimeouts: 3
17360
+ maxBashTimeouts: 3,
17361
+ warmSessionTtlSec: 1800
16845
17362
  };
16846
17363
  var MissingRequiredOptionError = class extends Error {
16847
17364
  constructor(flag) {
@@ -16865,6 +17382,7 @@ function parseCommonOptions(args) {
16865
17382
  flushIntervalMs: parseNonNegativeInt(args["flush-interval-ms"], "flush-interval-ms", DEFAULTS.flushIntervalMs),
16866
17383
  maxTurns: parseNonNegativeInt(args["max-turns"], "max-turns", DEFAULTS.maxTurns),
16867
17384
  maxBashTimeouts: parseNonNegativeInt(args["max-bash-timeouts"], "max-bash-timeouts", DEFAULTS.maxBashTimeouts),
17385
+ warmSessionTtlSec: parseNonNegativeInt(args["warm-session-ttl-sec"], "warm-session-ttl-sec", DEFAULTS.warmSessionTtlSec),
16868
17386
  debug: args.debug === true
16869
17387
  };
16870
17388
  }
@@ -16900,6 +17418,7 @@ function commonOptionDefs() {
16900
17418
  "flush-interval-ms": { type: "string" },
16901
17419
  "max-turns": { type: "string" },
16902
17420
  "max-bash-timeouts": { type: "string" },
17421
+ "warm-session-ttl-sec": { type: "string" },
16903
17422
  debug: { type: "boolean" }
16904
17423
  };
16905
17424
  }
@@ -16982,6 +17501,18 @@ function findUp(startDir, filename) {
16982
17501
  }
16983
17502
  }
16984
17503
  //#endregion
17504
+ //#region src/lib/state-dir.ts
17505
+ function ensureDaemonStateDirs(mountPath) {
17506
+ const rootDir = join(mountPath, ".moltnet", "d");
17507
+ const piSessionsDir = join(rootDir, "pi-sessions");
17508
+ mkdirSync(piSessionsDir, { recursive: true });
17509
+ return {
17510
+ rootDir,
17511
+ piSessionsDir,
17512
+ registryDbPath: join(rootDir, "daemon-state.sqlite")
17513
+ };
17514
+ }
17515
+ //#endregion
16985
17516
  //#region src/lib/turn-event-logger.ts
16986
17517
  function makeTurnEventHandler(base, context = {}) {
16987
17518
  const log = base.child({
@@ -17053,6 +17584,19 @@ async function runPolling(opts) {
17053
17584
  const listLimit = optionalPositiveInt(values["list-limit"], "list-limit", 10);
17054
17585
  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
17586
  const sandbox = resolveSandbox(process.cwd(), values.sandbox);
17587
+ const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
17588
+ const slotRegistry = new DaemonSlotRegistry(stateDirs.registryDbPath);
17589
+ const slotIdentity = {
17590
+ agentName: common.agent,
17591
+ provider: common.provider,
17592
+ model: common.model
17593
+ };
17594
+ const mainRepo = findMainWorktree();
17595
+ const executionPlans = createExecutionPlanCache({
17596
+ stateDirs,
17597
+ slotIdentity,
17598
+ warmSessionTtlSec: common.warmSessionTtlSec
17599
+ });
17056
17600
  const ctx = await resolveAgentContext(common.agent);
17057
17601
  const cfg = loadConfig();
17058
17602
  const otelShutdown = await initWorkerOtel({
@@ -17096,13 +17640,20 @@ async function runPolling(opts) {
17096
17640
  maxPollIntervalMs
17097
17641
  }, "agent-daemon.starting");
17098
17642
  const outputs = [];
17643
+ const subagentContractRegistry = createSubagentContractRegistry([{
17644
+ name: "judge_eval_variant_result",
17645
+ description: "Per-variant grading result produced by a subagent of judge_eval_variant: scores against the shared rubric, composite, and a 1-3 sentence verdict for a single variant.",
17646
+ parametersSchema: JudgeEvalVariantResult
17647
+ }]);
17099
17648
  try {
17100
17649
  const executeTask = createPiTaskExecutor({
17101
17650
  agentName: common.agent,
17651
+ subagentContractRegistry,
17102
17652
  mountPath: sandbox.rootDir,
17103
17653
  provider: common.provider,
17104
17654
  model: common.model,
17105
17655
  sandboxConfig: sandbox.config,
17656
+ makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
17106
17657
  makeOnTurnEvent: makeTurnEventHandlerFactory(rootLogger),
17107
17658
  maxTurns: common.maxTurns,
17108
17659
  maxBashTimeouts: common.maxBashTimeouts
@@ -17141,6 +17692,34 @@ async function runPolling(opts) {
17141
17692
  log: (msg, err) => rootLogger.warn({ err }, msg)
17142
17693
  }),
17143
17694
  executeTask: async (claimedTask, reporter) => {
17695
+ const executionPlan = executionPlans.getOrCreate(claimedTask);
17696
+ const sessionDescriptor = executionPlan.descriptor;
17697
+ let expired;
17698
+ try {
17699
+ expired = slotRegistry.reapExpiredSlots();
17700
+ if (expired.length > 0) rootLogger.info({
17701
+ expiredCount: expired.length,
17702
+ slotKeys: expired.map((item) => item.slot.slotKey)
17703
+ }, "agent-daemon.daemon_slots_reaped");
17704
+ } catch (err) {
17705
+ rootLogger.error({
17706
+ phase: "daemon_slot_reap",
17707
+ err: err instanceof Error ? err.message : String(err)
17708
+ }, "agent-daemon.daemon_slot_reap_failed");
17709
+ }
17710
+ rootLogger.debug({
17711
+ taskId: claimedTask.task.id,
17712
+ taskType: claimedTask.task.taskType,
17713
+ resumable: sessionDescriptor.policy.resumable,
17714
+ workspaceMode: sessionDescriptor.policy.workspaceMode,
17715
+ workspaceScope: sessionDescriptor.policy.workspaceScope,
17716
+ sessionScope: sessionDescriptor.policy.sessionScope,
17717
+ slotKey: executionPlan.slotKey,
17718
+ slotId: executionPlan.slotId,
17719
+ sessionKey: executionPlan.sessionKey,
17720
+ piSessionDir: executionPlan.sessionPersistence?.sessionDir ?? null,
17721
+ workspaceId: executionPlan.workspaceId
17722
+ }, "agent-daemon.task_execution_policy");
17144
17723
  if (taskTypes.length > 0 && !taskTypes.includes(claimedTask.task.taskType)) return {
17145
17724
  taskId: claimedTask.task.id,
17146
17725
  attemptN: claimedTask.attemptN,
@@ -17175,7 +17754,25 @@ async function runPolling(opts) {
17175
17754
  retryable: false
17176
17755
  }
17177
17756
  };
17178
- return executeTask(claimedTask, reporter);
17757
+ if (executionPlan.slotKey && executionPlan.sessionPersistence) slotRegistry.beginSlot({
17758
+ ...slotIdentity,
17759
+ slotKey: executionPlan.slotKey,
17760
+ taskType: claimedTask.task.taskType,
17761
+ sessionDir: executionPlan.sessionPersistence.sessionDir,
17762
+ sessionPath: resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir),
17763
+ workspaceId: executionPlan.workspaceId,
17764
+ worktreePath: executionPlan.workspaceId ? resolveTaskWorktreePath(mainRepo, executionPlan.workspaceId) : null,
17765
+ worktreeBranch: executionPlan.worktreeBranch,
17766
+ lastTaskId: claimedTask.task.id,
17767
+ lastAttemptN: claimedTask.attemptN,
17768
+ ttlSec: common.warmSessionTtlSec
17769
+ });
17770
+ try {
17771
+ return await executeTask(claimedTask, reporter);
17772
+ } finally {
17773
+ executionPlans.delete(claimedTask);
17774
+ if (executionPlan.slotKey) slotRegistry.finishSlot(slotIdentity, executionPlan.slotKey, common.warmSessionTtlSec, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null);
17775
+ }
17179
17776
  }
17180
17777
  });
17181
17778
  const drained = await runtime.start();
@@ -17183,6 +17780,7 @@ async function runPolling(opts) {
17183
17780
  rootLogger.info({ processed: drained.length }, "agent-daemon.drained");
17184
17781
  return drained.some((o) => o.status !== "completed") ? 1 : 0;
17185
17782
  } finally {
17783
+ slotRegistry.close();
17186
17784
  await otelShutdown();
17187
17785
  await shutdownLogger();
17188
17786
  }
@@ -17243,6 +17841,19 @@ async function runOnce(argv) {
17243
17841
  throw err;
17244
17842
  }
17245
17843
  const sandbox = resolveSandbox(process.cwd(), values.sandbox);
17844
+ const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
17845
+ const slotRegistry = new DaemonSlotRegistry(stateDirs.registryDbPath);
17846
+ const slotIdentity = {
17847
+ agentName: opts.agent,
17848
+ provider: opts.provider,
17849
+ model: opts.model
17850
+ };
17851
+ const mainRepo = findMainWorktree();
17852
+ const executionPlans = createExecutionPlanCache({
17853
+ stateDirs,
17854
+ slotIdentity,
17855
+ warmSessionTtlSec: opts.warmSessionTtlSec
17856
+ });
17246
17857
  const ctx = await resolveAgentContext(opts.agent);
17247
17858
  const cfg = loadConfig();
17248
17859
  const otelShutdown = await initWorkerOtel({
@@ -17290,17 +17901,58 @@ async function runOnce(argv) {
17290
17901
  process.on("SIGTERM", () => {
17291
17902
  onSignal("SIGTERM");
17292
17903
  });
17904
+ const subagentContractRegistry = createSubagentContractRegistry([{
17905
+ name: "judge_eval_variant_result",
17906
+ description: "Per-variant grading result produced by a subagent of judge_eval_variant: scores against the shared rubric, composite, and a 1-3 sentence verdict for a single variant.",
17907
+ parametersSchema: JudgeEvalVariantResult
17908
+ }]);
17293
17909
  try {
17294
- const executeTask = createPiTaskExecutor({
17910
+ const rawExecuteTask = createPiTaskExecutor({
17295
17911
  agentName: opts.agent,
17912
+ subagentContractRegistry,
17296
17913
  mountPath: sandbox.rootDir,
17297
17914
  provider: opts.provider,
17298
17915
  model: opts.model,
17299
17916
  sandboxConfig: sandbox.config,
17917
+ makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
17300
17918
  onTurnEvent: makeTurnEventHandler(rootLogger, { taskId }),
17301
17919
  maxTurns: opts.maxTurns,
17302
17920
  maxBashTimeouts: opts.maxBashTimeouts
17303
17921
  });
17922
+ const executeTask = async (claimedTask, reporter) => {
17923
+ try {
17924
+ const expired = slotRegistry.reapExpiredSlots();
17925
+ if (expired.length > 0) rootLogger.info({
17926
+ expiredCount: expired.length,
17927
+ slotKeys: expired.map((item) => item.slot.slotKey)
17928
+ }, "agent-daemon.daemon_slots_reaped");
17929
+ } catch (err) {
17930
+ rootLogger.error({
17931
+ phase: "daemon_slot_reap",
17932
+ err: err instanceof Error ? err.message : String(err)
17933
+ }, "agent-daemon.daemon_slot_reap_failed");
17934
+ }
17935
+ const executionPlan = executionPlans.getOrCreate(claimedTask);
17936
+ if (executionPlan.slotKey && executionPlan.sessionPersistence) slotRegistry.beginSlot({
17937
+ ...slotIdentity,
17938
+ slotKey: executionPlan.slotKey,
17939
+ taskType: claimedTask.task.taskType,
17940
+ sessionDir: executionPlan.sessionPersistence.sessionDir,
17941
+ sessionPath: resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir),
17942
+ workspaceId: executionPlan.workspaceId,
17943
+ worktreePath: executionPlan.workspaceId ? resolveTaskWorktreePath(mainRepo, executionPlan.workspaceId) : null,
17944
+ worktreeBranch: executionPlan.worktreeBranch,
17945
+ lastTaskId: claimedTask.task.id,
17946
+ lastAttemptN: claimedTask.attemptN,
17947
+ ttlSec: opts.warmSessionTtlSec
17948
+ });
17949
+ try {
17950
+ return await rawExecuteTask(claimedTask, reporter);
17951
+ } finally {
17952
+ executionPlans.delete(claimedTask);
17953
+ if (executionPlan.slotKey) slotRegistry.finishSlot(slotIdentity, executionPlan.slotKey, opts.warmSessionTtlSec, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null);
17954
+ }
17955
+ };
17304
17956
  const writeCorrelationAnchors = makePrBodyAnchorWriter({
17305
17957
  gh: createGhCliClient(),
17306
17958
  logger: rootLogger
@@ -17335,6 +17987,7 @@ async function runOnce(argv) {
17335
17987
  console.log(JSON.stringify(output, null, 2));
17336
17988
  return output.status === "completed" ? 0 : 1;
17337
17989
  } finally {
17990
+ slotRegistry.close();
17338
17991
  await otelShutdown();
17339
17992
  await shutdownLogger();
17340
17993
  }