@themoltnet/pi-extension 0.16.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -140,6 +140,7 @@ the base snapshot is used (Alpine + git + gh + MoltNet CLI + agent user).
140
140
  "cpus": 2,
141
141
  "memory": "6G"
142
142
  },
143
+ "resumeCommands": ["corepack enable"],
143
144
  "snapshot": {
144
145
  "allowedHosts": ["unofficial-builds.nodejs.org"],
145
146
  "overlaySize": "8G",
@@ -175,6 +176,25 @@ VM resource limits applied at runtime.
175
176
  | `cpus` | Number of virtual CPUs |
176
177
  | `memory` | RAM limit (e.g. `"6G"`) |
177
178
 
179
+ ### `resumeCommands`
180
+
181
+ Shell commands that run on every VM resume, after platform setup and before the
182
+ agent session starts.
183
+
184
+ Use this for per-session bootstrap that should not invalidate the snapshot
185
+ cache: mounting tmpfs, warming package-manager state, lightweight repo-local
186
+ setup.
187
+
188
+ Important properties:
189
+
190
+ - runs after TLS, DNS, and `git safe.directory` setup
191
+ - not part of the snapshot cache key
192
+ - each command runs in a fresh shell with `set -eu` and `set -o pipefail`
193
+ - first non-zero exit aborts VM resume
194
+
195
+ This split exists so repo-specific bootstrap can live in `sandbox.json` while
196
+ `pi-extension` stays consumer-agnostic.
197
+
178
198
  ### `vfs`
179
199
 
180
200
  VFS shadow configuration — hide host paths from the guest mount.
@@ -187,6 +207,31 @@ VFS shadow configuration — hide host paths from the guest mount.
187
207
  Use `shadow: ["node_modules"]` to hide host binaries (wrong platform) and let
188
208
  the guest install its own with `pnpm install`.
189
209
 
210
+ #### VFS caveat: shadowing is not a pnpm performance fix
211
+
212
+ For this repo we hit two distinct `/workspace` problems:
213
+
214
+ - the FUSE bridge makes file-write-heavy installs much slower than guest-local filesystems
215
+ - the `/workspace` VFS path drops `chmod()` calls, which breaks tools that create files and chmod them later
216
+
217
+ Dogfood trail:
218
+
219
+ - `47b67636-067a-4254-9098-38d00b4867bb` — `/workspace` install path measured at roughly 80x slower than guest tmpfs
220
+ - `62082ec9-0554-4bdc-9c64-9d89ece3fa40` — `chmod()` gap on the workspace mount
221
+ - `17f0ac6f-07f0-4e12-b5e5-d35a0fa2df6c` — first 100x pnpm recipe
222
+ - `2e4e25a9-ef4b-46bf-a55d-6c2b1159ee61` — follow-up fix for per-workspace `node_modules`
223
+
224
+ `vfs.shadow: ["node_modules"]` is still useful to hide host-built artifacts,
225
+ but it does not solve the hot-path problem by itself. For fast pnpm setup, move
226
+ both endpoints off the FUSE bridge:
227
+
228
+ - package store on guest-local disk, e.g. `NPM_CONFIG_STORE_DIR=/opt/pnpm-store`
229
+ - install target on guest tmpfs via `resumeCommands`
230
+
231
+ Current themoltnet `sandbox.json` does this by mounting tmpfs over the root and
232
+ per-workspace `node_modules` directories before running `pnpm install
233
+ --frozen-lockfile`.
234
+
190
235
  ### `env`
191
236
 
192
237
  Environment variable overrides applied to the guest VM. Use this to fix host
package/dist/index.d.ts CHANGED
@@ -32,9 +32,11 @@ import { WriteOperations } from '@earendil-works/pi-coding-agent';
32
32
  export declare function activateAgentEnv(agentEnv: Record<string, string | undefined>, repoRoot: string): void;
33
33
 
34
34
  /**
35
- * Construct an in-memory `AgentSession`. The caller is responsible for
36
- * eventually invoking `session.prompt(...)` and for tearing down — the
37
- * helper does no lifecycle management beyond construction.
35
+ * Construct an `AgentSession`. By default it is in-memory; callers may opt
36
+ * parent sessions into daemon-owned file persistence via `sessionPersistence`.
37
+ * The caller is responsible for eventually invoking `session.prompt(...)` and
38
+ * for tearing down — the helper does no lifecycle management beyond
39
+ * construction.
38
40
  */
39
41
  export declare function buildAgentSession(args: BuildAgentSessionArgs): Promise<AgentSession>;
40
42
 
@@ -56,6 +58,13 @@ declare interface BuildAgentSessionArgs {
56
58
  otelSpanAttrs: Record<string, string | number | boolean>;
57
59
  /** Agent name for `gen_ai.agent.name` on the root span. */
58
60
  agentName: string;
61
+ /**
62
+ * Parent sessions may persist their conversation history in a daemon-owned
63
+ * directory. Subagents should leave this unset and stay in-memory.
64
+ */
65
+ sessionPersistence?: {
66
+ sessionDir: string;
67
+ };
59
68
  }
60
69
 
61
70
  declare interface ClaimedTask {
@@ -277,6 +286,12 @@ export declare interface ExecutePiTaskOptions {
277
286
  * after HOST_EXEC_ALLOWED; an array limits auto-approval to matching rules.
278
287
  */
279
288
  hostExecAutoApprove?: HostExecAutoApproveConfig;
289
+ /**
290
+ * Optional daemon-supplied execution plan. Keeps task semantics out of
291
+ * `pi-extension` while still letting callers opt into stable worktrees and
292
+ * file-backed Pi sessions for selected task classes.
293
+ */
294
+ makeExecutionPlan?: PiTaskExecutionPlanFactory;
280
295
  }
281
296
 
282
297
  /**
@@ -415,6 +430,42 @@ export declare interface PiOtelOptions {
415
430
  spanAttributes?: Record<string, string | number | boolean>;
416
431
  }
417
432
 
433
+ export declare interface PiSessionPersistencePlan {
434
+ sessionDir: string;
435
+ }
436
+
437
+ export declare interface PiTaskExecutionPlan {
438
+ /**
439
+ * Daemon-local reuse key. When set alongside `workspaceScope: 'session'`,
440
+ * dedicated worktrees may be retained and reopened across related tasks.
441
+ */
442
+ sessionKey: string | null;
443
+ /**
444
+ * Workspace identity selected by the daemon. `null` means the task should
445
+ * run against the shared mount path.
446
+ */
447
+ workspaceId: string | null;
448
+ /**
449
+ * Branch to create or reopen for the workspace. `null` means no dedicated
450
+ * worktree is required.
451
+ */
452
+ worktreeBranch: string | null;
453
+ /**
454
+ * Lifetime of the task workspace from the daemon's point of view.
455
+ * `attempt` = disposable; `session` = keep stable for the reuse key.
456
+ */
457
+ workspaceScope: 'attempt' | 'session';
458
+ /**
459
+ * Optional location for file-backed Pi session history. When omitted,
460
+ * the executor keeps the conversation in memory for this attempt only.
461
+ */
462
+ sessionPersistence?: PiSessionPersistencePlan | null;
463
+ }
464
+
465
+ export declare type PiTaskExecutionPlanFactory = (claimedTask: ClaimedTask) => PiTaskExecutionPlan | null;
466
+
467
+ export declare function resolveTaskWorktreePath(mainRepo: string, workspaceId: string): string;
468
+
418
469
  /**
419
470
  * Resume a VM from a checkpoint, inject credentials, configure egress +
420
471
  * TLS. Returns the managed VM handle.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
4
- import path, { join } from "node:path";
4
+ import path, { join, relative } from "node:path";
5
5
  import { DefaultResourceLoader, SessionManager, createAgentSession, createBashTool, createBashToolDefinition, createEditTool, createEditToolDefinition, createReadTool, createReadToolDefinition, createSyntheticSourceInfo, createWriteTool, createWriteToolDefinition, defineTool, parseFrontmatter } from "@earendil-works/pi-coding-agent";
6
6
  import { createHash } from "node:crypto";
7
7
  import { readFile } from "node:fs/promises";
@@ -8633,9 +8633,11 @@ var NO_SKILLS = () => ({
8633
8633
  diagnostics: []
8634
8634
  });
8635
8635
  /**
8636
- * Construct an in-memory `AgentSession`. The caller is responsible for
8637
- * eventually invoking `session.prompt(...)` and for tearing down — the
8638
- * helper does no lifecycle management beyond construction.
8636
+ * Construct an `AgentSession`. By default it is in-memory; callers may opt
8637
+ * parent sessions into daemon-owned file persistence via `sessionPersistence`.
8638
+ * The caller is responsible for eventually invoking `session.prompt(...)` and
8639
+ * for tearing down — the helper does no lifecycle management beyond
8640
+ * construction.
8639
8641
  */
8640
8642
  async function buildAgentSession(args) {
8641
8643
  const piOtelExtension = createPiOtelExtension({
@@ -8650,15 +8652,23 @@ async function buildAgentSession(args) {
8650
8652
  skillsOverride: args.skillsOverride ?? NO_SKILLS
8651
8653
  });
8652
8654
  await resourceLoader.reload();
8655
+ const sessionManager = args.sessionPersistence ? await resolvePersistentSessionManager({
8656
+ cwd: args.mountPath,
8657
+ sessionDir: args.sessionPersistence.sessionDir
8658
+ }) : SessionManager.inMemory(args.mountPath);
8653
8659
  return (await createAgentSession({
8654
8660
  agentDir: args.piAuthDir,
8655
8661
  cwd: args.mountPath,
8656
8662
  model: args.modelHandle,
8657
8663
  customTools: args.customTools,
8658
- sessionManager: SessionManager.inMemory(),
8664
+ sessionManager,
8659
8665
  resourceLoader
8660
8666
  })).session;
8661
8667
  }
8668
+ async function resolvePersistentSessionManager(args) {
8669
+ await SessionManager.list(args.cwd, args.sessionDir);
8670
+ return SessionManager.continueRecent(args.cwd, args.sessionDir);
8671
+ }
8662
8672
  //#endregion
8663
8673
  //#region ../tasks/src/formats.ts
8664
8674
  /**
@@ -8824,22 +8834,6 @@ function validateRubricWeights(rubric) {
8824
8834
  if (Math.abs(sum - 1) > 1e-6) return `Rubric weights must sum to 1.0 (got ${sum.toFixed(6)})`;
8825
8835
  return null;
8826
8836
  }
8827
- `
8828
- You are reviewing a GitHub pull request for **complexity** — how hard
8829
- this change is to review safely, NOT whether it's correct or whether
8830
- the feature is worthwhile. The diff has already been opened by the
8831
- producer; your job is to score reviewability.
8832
-
8833
- You may run \`gh pr diff <number>\`, \`gh pr view <number>\`, and read
8834
- files in the workspace. Don't run tests, don't push commits, don't
8835
- modify anything. The PR's GitHub URL is in the target metadata.
8836
-
8837
- When in doubt about a criterion, score conservatively (lower) and
8838
- explain what made the call ambiguous. Reviewers will read your
8839
- rationale; "looks fine" is not useful, "the change touches three
8840
- unrelated subsystems and the test coverage on the auth path is
8841
- unchanged" is.
8842
- `.trim();
8843
8837
  //#endregion
8844
8838
  //#region ../tasks/src/success-criteria.ts
8845
8839
  /**
@@ -9718,6 +9712,10 @@ var BUILT_IN_TASK_TYPES = {
9718
9712
  inputSchema: FulfillBriefInput,
9719
9713
  outputSchema: FulfillBriefOutput,
9720
9714
  outputKind: "artifact",
9715
+ resumable: true,
9716
+ workspaceMode: "dedicated_worktree",
9717
+ workspaceScope: "session",
9718
+ sessionScope: "correlation",
9721
9719
  requiresReferences: false,
9722
9720
  validateOutput: requireVerificationWhenCriteriaPresent
9723
9721
  },
@@ -9726,6 +9724,9 @@ var BUILT_IN_TASK_TYPES = {
9726
9724
  inputSchema: AssessBriefInput,
9727
9725
  outputSchema: AssessBriefOutput,
9728
9726
  outputKind: "judgment",
9727
+ workspaceMode: "dedicated_worktree",
9728
+ workspaceScope: "attempt",
9729
+ sessionScope: "none",
9729
9730
  requiresReferences: true,
9730
9731
  validateInput: validateJudgmentInput,
9731
9732
  validateInputAsync: validateAssessBriefInputAsync
@@ -9735,6 +9736,8 @@ var BUILT_IN_TASK_TYPES = {
9735
9736
  inputSchema: CuratePackInput,
9736
9737
  outputSchema: CuratePackOutput,
9737
9738
  outputKind: "artifact",
9739
+ workspaceScope: "attempt",
9740
+ sessionScope: "none",
9738
9741
  requiresReferences: false,
9739
9742
  validateOutput: requireVerificationWhenCriteriaPresent
9740
9743
  },
@@ -9743,6 +9746,8 @@ var BUILT_IN_TASK_TYPES = {
9743
9746
  inputSchema: RenderPackInput,
9744
9747
  outputSchema: RenderPackOutput,
9745
9748
  outputKind: "artifact",
9749
+ workspaceScope: "attempt",
9750
+ sessionScope: "none",
9746
9751
  requiresReferences: false,
9747
9752
  validateOutput: requireVerificationWhenCriteriaPresent,
9748
9753
  validateInputAsync: validateRenderPackInputAsync
@@ -9752,6 +9757,8 @@ var BUILT_IN_TASK_TYPES = {
9752
9757
  inputSchema: JudgePackInput,
9753
9758
  outputSchema: JudgePackOutput,
9754
9759
  outputKind: "judgment",
9760
+ workspaceScope: "attempt",
9761
+ sessionScope: "none",
9755
9762
  requiresReferences: true,
9756
9763
  validateInput: validateJudgmentInput,
9757
9764
  validateOutput: validateJudgePackOutput,
@@ -9762,6 +9769,8 @@ var BUILT_IN_TASK_TYPES = {
9762
9769
  inputSchema: RunEvalInput,
9763
9770
  outputSchema: RunEvalOutput,
9764
9771
  outputKind: "artifact",
9772
+ workspaceScope: "attempt",
9773
+ sessionScope: "custom",
9765
9774
  requiresReferences: false,
9766
9775
  validateOutput: validateRunEvalOutput
9767
9776
  },
@@ -9770,6 +9779,8 @@ var BUILT_IN_TASK_TYPES = {
9770
9779
  inputSchema: JudgeEvalVariantInput,
9771
9780
  outputSchema: JudgeEvalVariantOutput,
9772
9781
  outputKind: "judgment",
9782
+ workspaceScope: "attempt",
9783
+ sessionScope: "custom",
9773
9784
  requiresReferences: false,
9774
9785
  validateInput: validateJudgeEvalVariantInput,
9775
9786
  validateOutput: validateJudgeEvalVariantOutput,
@@ -10326,6 +10337,15 @@ function buildAssessBriefUserPrompt(input, ctx) {
10326
10337
  rubric.preamble,
10327
10338
  ""
10328
10339
  ].join("\n") : "";
10340
+ const workspaceSection = ctx.workspace?.mode === "dedicated_worktree" ? [
10341
+ "### Workspace",
10342
+ "",
10343
+ "This review attempt is running inside a dedicated disposable git",
10344
+ "worktree created for this task. If you need to check out the target",
10345
+ "branch or inspect refs locally, do it only inside this worktree.",
10346
+ ctx.workspace.branch ? `The current review branch is \`${ctx.workspace.branch}\`. You may replace it with the target branch locally if that helps your inspection.` : "The current checkout is disposable and will be cleaned up when the task ends.",
10347
+ ""
10348
+ ].join("\n") : "";
10329
10349
  return [
10330
10350
  "# Assess Brief Judge",
10331
10351
  "",
@@ -10366,6 +10386,7 @@ function buildAssessBriefUserPrompt(input, ctx) {
10366
10386
  " read it from the task you fetched in step 1 and pass",
10367
10387
  " `taskFilter: { correlationId: \"<id>\" }`.",
10368
10388
  "",
10389
+ workspaceSection,
10369
10390
  preambleSection,
10370
10391
  "## Criteria",
10371
10392
  "",
@@ -10625,6 +10646,14 @@ function buildFulfillBriefUserPrompt(input, ctx) {
10625
10646
  "from this branch naming scheme when correlationId is set.",
10626
10647
  ""
10627
10648
  ].join("\n") : "";
10649
+ const workspaceSection = ctx.workspace?.mode === "dedicated_worktree" ? [
10650
+ "### Workspace",
10651
+ "",
10652
+ "This attempt is running inside a dedicated git worktree created",
10653
+ "for this task. Do not repurpose or switch the primary checkout.",
10654
+ ctx.workspace.branch ? `The current branch is \`${ctx.workspace.branch}\`. Stay on this branch unless the runtime instructor explicitly tells you otherwise.` : "Stay on the branch that was pre-provisioned for this task.",
10655
+ ""
10656
+ ].join("\n") : "";
10628
10657
  return [
10629
10658
  "# Fulfill Brief Agent",
10630
10659
  "",
@@ -10645,9 +10674,10 @@ function buildFulfillBriefUserPrompt(input, ctx) {
10645
10674
  criteriaSection,
10646
10675
  seedSection,
10647
10676
  correlationSection,
10677
+ workspaceSection,
10648
10678
  "### Workflow",
10649
10679
  "",
10650
- `1. Create a feature branch (starting prefix suggestion: \`${branchSlug}<short-slug>\`).`,
10680
+ ctx.workspace?.mode === "dedicated_worktree" ? `1. Use the already-provisioned dedicated worktree branch${ctx.workspace.branch ? ` (\`${ctx.workspace.branch}\`)` : ""}; do not create or switch the primary checkout.` : `1. Create a feature branch (starting prefix suggestion: \`${branchSlug}<short-slug>\`).`,
10651
10681
  "2. Understand the problem — read relevant code; do not speculate.",
10652
10682
  "3. Implement the change. Keep commits small and coherent.",
10653
10683
  "4. Add tests if applicable.",
@@ -11045,7 +11075,8 @@ function buildTaskUserPrompt(task, ctx) {
11045
11075
  return buildFulfillBriefUserPrompt(task.input, {
11046
11076
  diaryId: ctx.diaryId,
11047
11077
  taskId: ctx.taskId,
11048
- correlationId: task.correlationId
11078
+ correlationId: task.correlationId,
11079
+ workspace: ctx.workspace
11049
11080
  });
11050
11081
  case ASSESS_BRIEF_TYPE:
11051
11082
  if (!Value.Check(AssessBriefInput, task.input)) {
@@ -11054,7 +11085,8 @@ function buildTaskUserPrompt(task, ctx) {
11054
11085
  }
11055
11086
  return buildAssessBriefUserPrompt(task.input, {
11056
11087
  diaryId: ctx.diaryId,
11057
- taskId: ctx.taskId
11088
+ taskId: ctx.taskId,
11089
+ workspace: ctx.workspace
11058
11090
  });
11059
11091
  case CURATE_PACK_TYPE:
11060
11092
  if (!Value.Check(CuratePackInput, task.input)) {
@@ -15182,6 +15214,113 @@ function resolveSubmitTools(taskType, opts = {}) {
15182
15214
  };
15183
15215
  }
15184
15216
  //#endregion
15217
+ //#region src/runtime/task-workspace.ts
15218
+ function prepareTaskWorkspace(task, requestedMountPath, executionPlan) {
15219
+ const branch = executionPlan?.worktreeBranch ?? null;
15220
+ if (!branch) return {
15221
+ mountPath: requestedMountPath,
15222
+ mode: "shared_mount",
15223
+ branch: null,
15224
+ cleanup: () => {}
15225
+ };
15226
+ const mainRepo = findMainWorktree();
15227
+ const worktreeDir = resolveTaskWorktreePath(mainRepo, executionPlan?.workspaceId ?? `task-${task.id}`);
15228
+ const relMount = relative(mainRepo, requestedMountPath);
15229
+ const mountPath = relMount === "" || relMount.startsWith("..") ? worktreeDir : join(worktreeDir, relMount);
15230
+ const keepWorkspace = executionPlan?.workspaceScope === "session" && executionPlan.sessionKey !== null;
15231
+ if (keepWorkspace) ensureReusableTaskWorktree(mainRepo, worktreeDir, branch);
15232
+ else {
15233
+ removeExistingTaskWorktree(mainRepo, worktreeDir);
15234
+ addTaskWorktree(mainRepo, worktreeDir, branch);
15235
+ }
15236
+ return {
15237
+ mountPath,
15238
+ mode: "dedicated_worktree",
15239
+ branch,
15240
+ cleanup: keepWorkspace ? () => {} : () => {
15241
+ execFileSync("git", [
15242
+ "-C",
15243
+ mainRepo,
15244
+ "worktree",
15245
+ "remove",
15246
+ "--force",
15247
+ worktreeDir
15248
+ ], { stdio: "pipe" });
15249
+ }
15250
+ };
15251
+ }
15252
+ function resolveTaskWorktreePath(mainRepo, workspaceId) {
15253
+ return join(mainRepo, ".worktrees", workspaceId);
15254
+ }
15255
+ function ensureReusableTaskWorktree(mainRepo, worktreeDir, branch) {
15256
+ if (isRegisteredWorktree(mainRepo, worktreeDir)) return;
15257
+ if (existsSync(worktreeDir)) throw new Error(`Expected reusable worktree ${worktreeDir} to be git-managed, but it exists outside git worktree metadata.`);
15258
+ addTaskWorktree(mainRepo, worktreeDir, branch);
15259
+ }
15260
+ function addTaskWorktree(mainRepo, worktreeDir, branch) {
15261
+ const baseRef = resolveWorktreeBaseRef(mainRepo);
15262
+ execFileSync("git", gitRefExists(mainRepo, `refs/heads/${branch}`) ? [
15263
+ "-C",
15264
+ mainRepo,
15265
+ "worktree",
15266
+ "add",
15267
+ worktreeDir,
15268
+ branch
15269
+ ] : [
15270
+ "-C",
15271
+ mainRepo,
15272
+ "worktree",
15273
+ "add",
15274
+ "-b",
15275
+ branch,
15276
+ worktreeDir,
15277
+ baseRef
15278
+ ], { stdio: "pipe" });
15279
+ }
15280
+ function removeExistingTaskWorktree(mainRepo, worktreeDir) {
15281
+ if (!existsSync(worktreeDir) || !isRegisteredWorktree(mainRepo, worktreeDir)) return;
15282
+ execFileSync("git", [
15283
+ "-C",
15284
+ mainRepo,
15285
+ "worktree",
15286
+ "remove",
15287
+ "--force",
15288
+ worktreeDir
15289
+ ], { stdio: "pipe" });
15290
+ }
15291
+ function isRegisteredWorktree(mainRepo, worktreeDir) {
15292
+ const list = execFileSync("git", [
15293
+ "-C",
15294
+ mainRepo,
15295
+ "worktree",
15296
+ "list",
15297
+ "--porcelain"
15298
+ ], {
15299
+ encoding: "utf8",
15300
+ stdio: "pipe"
15301
+ });
15302
+ const marker = `worktree ${worktreeDir}\n`;
15303
+ return list.includes(marker) || list.endsWith(`worktree ${worktreeDir}`);
15304
+ }
15305
+ function resolveWorktreeBaseRef(mainRepo) {
15306
+ return gitRefExists(mainRepo, "refs/heads/main") ? "main" : "HEAD";
15307
+ }
15308
+ function gitRefExists(mainRepo, ref) {
15309
+ try {
15310
+ execFileSync("git", [
15311
+ "-C",
15312
+ mainRepo,
15313
+ "show-ref",
15314
+ "--verify",
15315
+ "--quiet",
15316
+ ref
15317
+ ], { stdio: "pipe" });
15318
+ return true;
15319
+ } catch {
15320
+ return false;
15321
+ }
15322
+ }
15323
+ //#endregion
15185
15324
  //#region src/runtime/execute-pi-task.ts
15186
15325
  /**
15187
15326
  * executePiTask — run a single Task attempt using pi-coding-agent inside a
@@ -15230,7 +15369,10 @@ async function executePiTask(claimedTask, reporter, opts) {
15230
15369
  const task = claimedTask.task;
15231
15370
  const attemptN = claimedTask.attemptN;
15232
15371
  const startTime = Date.now();
15233
- const mountPath = opts.mountPath ?? process.cwd();
15372
+ const requestedMountPath = opts.mountPath ?? process.cwd();
15373
+ const executionPlan = opts.makeExecutionPlan?.(claimedTask) ?? null;
15374
+ const workspace = prepareTaskWorkspace(task, requestedMountPath, executionPlan);
15375
+ const mountPath = workspace.mountPath;
15234
15376
  if (reporter.cancelSignal.aborted) return {
15235
15377
  taskId: task.id,
15236
15378
  attemptN,
@@ -15261,7 +15403,8 @@ async function executePiTask(claimedTask, reporter, opts) {
15261
15403
  "--relative-paths"
15262
15404
  ], { stdio: "pipe" });
15263
15405
  } catch {}
15264
- const managed = await resumeVm({
15406
+ let managed = null;
15407
+ managed = await resumeVm({
15265
15408
  checkpointPath,
15266
15409
  agentName: opts.agentName,
15267
15410
  mountPath,
@@ -15321,13 +15464,19 @@ async function executePiTask(claimedTask, reporter, opts) {
15321
15464
  taskType: task.taskType,
15322
15465
  teamId: task.teamId,
15323
15466
  provider: opts.provider,
15324
- model: opts.model
15467
+ model: opts.model,
15468
+ workspaceMode: workspace.mode,
15469
+ workspaceBranch: workspace.branch
15325
15470
  });
15326
15471
  let taskPrompt;
15327
15472
  try {
15328
15473
  taskPrompt = buildTaskUserPrompt(task, {
15329
15474
  diaryId,
15330
15475
  taskId: task.id,
15476
+ workspace: {
15477
+ mode: workspace.mode,
15478
+ branch: workspace.branch
15479
+ },
15331
15480
  extras: opts.promptExtras
15332
15481
  });
15333
15482
  } catch (err) {
@@ -15438,7 +15587,8 @@ async function executePiTask(claimedTask, reporter, opts) {
15438
15587
  "moltnet.task.id": task.id,
15439
15588
  "moltnet.task.attempt": attemptN,
15440
15589
  "moltnet.task.type": task.taskType
15441
- }
15590
+ },
15591
+ sessionPersistence: executionPlan?.sessionPersistence ?? void 0
15442
15592
  });
15443
15593
  } catch (err) {
15444
15594
  const message = err instanceof Error ? err.message : String(err);
@@ -15657,7 +15807,13 @@ async function executePiTask(claimedTask, reporter, opts) {
15657
15807
  console.error(`executePiTask: reporter.close() failed for task ${task.id} attempt ${attemptN}: ${detail}`);
15658
15808
  }
15659
15809
  }
15660
- await managed.vm.close();
15810
+ if (managed) await managed.vm.close();
15811
+ try {
15812
+ workspace.cleanup();
15813
+ } catch (err) {
15814
+ const detail = err instanceof Error ? err.message : String(err);
15815
+ console.error(`executePiTask: workspace cleanup failed for task ${task.id} attempt ${attemptN}: ${detail}`);
15816
+ }
15661
15817
  }
15662
15818
  }
15663
15819
  function emptyUsage(provider, model) {
@@ -16039,4 +16195,4 @@ function moltnetExtension(pi) {
16039
16195
  registerMoltnetReflectCommand(pi, state);
16040
16196
  }
16041
16197
  //#endregion
16042
- export { HOST_EXEC_DEFAULT_BASE_ENV, activateAgentEnv, buildAgentSession, createGondolinBashOps, createGondolinEditOps, createGondolinReadOps, createGondolinWriteOps, createMoltNetTools, createPiOtelExtension, createPiTaskExecutor, createSubagentTool, moltnetExtension as default, ensureSnapshot, executePiTask, findMainWorktree, injectTaskContext, loadCredentials, resumeVm, toGuestPath };
16198
+ export { HOST_EXEC_DEFAULT_BASE_ENV, activateAgentEnv, buildAgentSession, createGondolinBashOps, createGondolinEditOps, createGondolinReadOps, createGondolinWriteOps, createMoltNetTools, createPiOtelExtension, createPiTaskExecutor, createSubagentTool, moltnetExtension as default, ensureSnapshot, executePiTask, findMainWorktree, injectTaskContext, loadCredentials, resolveTaskWorktreePath, resumeVm, toGuestPath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/pi-extension",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "description": "MoltNet pi extension — sandboxed tool execution in Gondolin VMs with MoltNet identity and persistent memory",
6
6
  "license": "MIT",
@@ -31,8 +31,8 @@
31
31
  "@earendil-works/gondolin": "^0.9.1",
32
32
  "@opentelemetry/api": "^1.9.0",
33
33
  "@sinclair/typebox": "^0.34.0",
34
- "@themoltnet/agent-runtime": "0.15.0",
35
- "@themoltnet/sdk": "0.102.0"
34
+ "@themoltnet/sdk": "0.102.0",
35
+ "@themoltnet/agent-runtime": "0.15.1"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@earendil-works/pi-coding-agent": ">=0.74.0",
@@ -51,7 +51,7 @@
51
51
  "@earendil-works/pi-coding-agent": "^0.74.0",
52
52
  "@opentelemetry/sdk-metrics": "^2.5.1",
53
53
  "@opentelemetry/sdk-trace-base": "^2.5.1",
54
- "@types/node": "^20.11.0",
54
+ "@types/node": "^22.19.0",
55
55
  "typescript": "^5.3.3",
56
56
  "vite": "^8.0.0",
57
57
  "vite-plugin-dts": "^4.5.4",