@themoltnet/pi-extension 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ import { EditOperations } from '@earendil-works/pi-coding-agent';
6
6
  import { ExtensionAPI } from '@earendil-works/pi-coding-agent';
7
7
  import { LoadSkillsResult } from '@earendil-works/pi-coding-agent';
8
8
  import { Model } from '@earendil-works/pi-ai';
9
+ import { Readable } from 'node:stream';
9
10
  import { ReadOperations } from '@earendil-works/pi-coding-agent';
10
11
  import { Skill } from '@earendil-works/pi-coding-agent';
11
12
  import { Static } from 'typebox';
@@ -144,6 +145,14 @@ export declare function createMoltNetTools(config: MoltNetToolsConfig): ToolDefi
144
145
 
145
146
  export declare function createPiOtelExtension(options?: PiOtelOptions): (pi: ExtensionAPI) => void;
146
147
 
148
+ export declare function createPiRetryTriage(options: {
149
+ model: Model<Api>;
150
+ thinkingLevel?: PiRetryTriageThinkingLevel | null;
151
+ piAgentDir: string;
152
+ timeoutMs?: number;
153
+ cwd?: string;
154
+ }): PiRetryTriage;
155
+
147
156
  /**
148
157
  * Factory that builds a pi-specific `executeTask` function suitable for
149
158
  * injection into `AgentRuntime`. The returned function caches the resolved
@@ -294,6 +303,8 @@ export declare interface ExecutePiTaskOptions {
294
303
  extraAllowedHosts?: string[];
295
304
  /** Sandbox overrides (env, VFS shadows, resources). */
296
305
  sandboxConfig?: SandboxConfig;
306
+ /** Host environment variable names to forward into the Pi VM. */
307
+ forwardEnv?: string[];
297
308
  /**
298
309
  * Forwarded to `buildTaskUserPrompt` for per-type builders. Static
299
310
  * across tasks. Today no built-in builder needs per-task `extras` —
@@ -481,6 +492,17 @@ declare interface MoltNetToolsConfig {
481
492
  clearSessionErrors(): void;
482
493
  /** Host working directory for host-exec commands (worktree path or cwd). */
483
494
  getHostCwd?(): string;
495
+ /**
496
+ * Optional workspace-file reader. Daemon/Gondolin callers provide this so
497
+ * artifact uploads see guest overlay writes that may not exist on the host
498
+ * mount path yet.
499
+ */
500
+ openWorkspaceFileForRead?(filePath: string): Promise<{
501
+ stream: Readable;
502
+ isFile: boolean;
503
+ sizeBytes?: number;
504
+ displayPath?: string;
505
+ }>;
484
506
  /**
485
507
  * Set of process.env keys that are safe to forward to host-exec child
486
508
  * processes. Configured at sandbox startup so the caller can include
@@ -510,6 +532,8 @@ declare interface MoltNetToolsConfig {
510
532
  getTaskContext?(): MoltNetTaskContext | null;
511
533
  }
512
534
 
535
+ export declare function normalizeRetryTriageResult(value: unknown): PiRetryTriageResult;
536
+
513
537
  export declare interface PiOtelOptions {
514
538
  /** Agent name for `gen_ai.agent.name` on the root span. */
515
539
  agentName?: string;
@@ -521,6 +545,38 @@ export declare interface PiOtelOptions {
521
545
  spanAttributes?: Record<string, string | number | boolean>;
522
546
  }
523
547
 
548
+ export declare type PiRetryTriage = (input: PiRetryTriageInput) => Promise<PiRetryTriageResult>;
549
+
550
+ export declare type PiRetryTriageConfidence = RetryTriageConfidence;
551
+
552
+ export declare type PiRetryTriageDecision = RetryTriageDecision;
553
+
554
+ export declare interface PiRetryTriageInput {
555
+ task: {
556
+ id: string;
557
+ taskType: string;
558
+ teamId: string;
559
+ input: unknown;
560
+ };
561
+ attemptN: number;
562
+ maxAttempts?: number | null;
563
+ remainingAttempts?: number | null;
564
+ error: unknown;
565
+ recentMessages?: {
566
+ timestamp: string;
567
+ kind: string;
568
+ payload: unknown;
569
+ }[];
570
+ }
571
+
572
+ export declare interface PiRetryTriageResult {
573
+ decision: RetryTriageDecision;
574
+ confidence: RetryTriageConfidence;
575
+ reason: string;
576
+ }
577
+
578
+ export declare type PiRetryTriageThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
579
+
524
580
  export declare interface PiSessionPersistencePlan {
525
581
  sessionDir: string;
526
582
  forkFromSessionPath?: string | null;
@@ -591,6 +647,8 @@ declare interface PiWorkspaceSeedPlan {
591
647
  source: 'producer';
592
648
  }
593
649
 
650
+ export declare function redactRetryTriageSecrets(value: string): string;
651
+
594
652
  export declare function resolveTaskWorktreePath(mainRepo: string, workspaceId: string): string;
595
653
 
596
654
  export declare interface ResumeCommand {
@@ -621,6 +679,10 @@ declare interface ResumeCommandWhen {
621
679
  */
622
680
  export declare function resumeVm(config: VmConfig): Promise<ManagedVm>;
623
681
 
682
+ export declare type RetryTriageConfidence = 'low' | 'medium' | 'high';
683
+
684
+ export declare type RetryTriageDecision = 'retry' | 'do_not_retry';
685
+
624
686
  export declare interface SandboxConfig {
625
687
  /** Snapshot build settings. */
626
688
  snapshot?: {
@@ -839,6 +901,12 @@ declare const TaskOutput: Type.TObject<{
839
901
  message: Type.TString;
840
902
  stack: Type.TOptional<Type.TString>;
841
903
  retryable: Type.TOptional<Type.TBoolean>;
904
+ retry: Type.TOptional<Type.TObject<{
905
+ source: Type.TUnion<[Type.TLiteral<"explicit">, Type.TLiteral<"deterministic">, Type.TLiteral<"attempts_exhausted">, Type.TLiteral<"triage">, Type.TLiteral<"triage_failed">]>;
906
+ decision: Type.TOptional<Type.TUnion<[Type.TLiteral<"retry">, Type.TLiteral<"do_not_retry">]>>;
907
+ confidence: Type.TOptional<Type.TUnion<[Type.TLiteral<"low">, Type.TLiteral<"medium">, Type.TLiteral<"high">]>>;
908
+ reason: Type.TOptional<Type.TString>;
909
+ }>>;
842
910
  }>>;
843
911
  contentSignature: Type.TOptional<Type.TString>;
844
912
  }>;
@@ -972,6 +1040,14 @@ export declare interface VmConfig {
972
1040
  extraAllowedHosts?: string[];
973
1041
  /** Full sandbox config (vfs shadows, env overrides). */
974
1042
  sandboxConfig?: SandboxConfig;
1043
+ /**
1044
+ * Host environment variable names to copy into the VM process.
1045
+ *
1046
+ * Runtime profiles use this for provider API keys: `requiredEnv` proves the
1047
+ * daemon host has the secret, and this allowlist forwards only those names
1048
+ * into the guest without storing secret values in the profile.
1049
+ */
1050
+ forwardEnv?: string[];
975
1051
  /** Abort resume/setup work, closing any live VM owned by resumeVm. */
976
1052
  signal?: AbortSignal;
977
1053
  }
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 { cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
4
- import path, { join, relative, sep } from "node:path";
4
+ import path, { isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { DEFAULT_MAX_BYTES, DefaultResourceLoader, SessionManager, createAgentSession, createBashTool, createBashToolDefinition, createEditTool, createEditToolDefinition, createFindTool, createFindToolDefinition, createGrepTool, createGrepToolDefinition, createLsTool, createLsToolDefinition, createReadTool, createReadToolDefinition, createSyntheticSourceInfo, createWriteTool, createWriteToolDefinition, defineTool, formatSize, parseFrontmatter, truncateHead, truncateLine } from "@earendil-works/pi-coding-agent";
6
6
  import { createHash } from "node:crypto";
7
7
  import { Readable } from "node:stream";
@@ -1159,7 +1159,7 @@ var listDiaryPacks = (options) => (options.client ?? client).get({
1159
1159
  ...options
1160
1160
  });
1161
1161
  /**
1162
- * Create and persist a custom context pack from an explicit entry selection.
1162
+ * Create and persist a custom context pack from an explicit entry selection. Returns 409 if any selected entry is flagged as a prompt-injection risk; the response lists the flagged entries. Set `force: true` to override and persist anyway.
1163
1163
  */
1164
1164
  var createDiaryCustomPack = (options) => (options.client ?? client).post({
1165
1165
  security: [
@@ -2159,7 +2159,7 @@ var completeTask = (options) => (options.client ?? client).post({
2159
2159
  /**
2160
2160
  * Mark an attempt as failed with error details.
2161
2161
  */
2162
- var failTask = (options) => (options.client ?? client).post({
2162
+ var failTaskAttempt = (options) => (options.client ?? client).post({
2163
2163
  security: [
2164
2164
  {
2165
2165
  scheme: "bearer",
@@ -2874,6 +2874,15 @@ function unwrapResult(result) {
2874
2874
  networkError.stack = error.stack;
2875
2875
  throw networkError;
2876
2876
  }
2877
+ const responseSummary = summarizeResponse(result.response);
2878
+ if (responseSummary) {
2879
+ const detail = stringifyUnknown(error);
2880
+ throw new MoltNetError(`MoltNet API request failed with HTTP ${responseSummary.status} ${responseSummary.statusText}: ${detail}`, {
2881
+ code: `HTTP_${responseSummary.status}`,
2882
+ detail,
2883
+ statusCode: responseSummary.status
2884
+ });
2885
+ }
2877
2886
  throw new MoltNetError(`Unexpected error from MoltNet API: ${stringifyUnknown(error)}`, { code: "UNKNOWN" });
2878
2887
  }
2879
2888
  if (result.data === void 0) throw new MoltNetError("Unexpected empty response from MoltNet API", { code: "EMPTY_RESPONSE" });
@@ -2891,6 +2900,15 @@ function stringifyUnknown(value) {
2891
2900
  return String(value);
2892
2901
  }
2893
2902
  }
2903
+ function summarizeResponse(response) {
2904
+ if (!response || typeof response !== "object") return null;
2905
+ const candidate = response;
2906
+ if (typeof candidate.status !== "number") return null;
2907
+ return {
2908
+ status: candidate.status,
2909
+ statusText: typeof candidate.statusText === "string" && candidate.statusText ? candidate.statusText : "Error"
2910
+ };
2911
+ }
2894
2912
  function unwrapRequired(result, message, code) {
2895
2913
  if (result.error || !result.data) throw new MoltNetError(message, { code });
2896
2914
  return result.data;
@@ -14001,6 +14019,27 @@ var TaskUsage = _Object_({
14001
14019
  $id: "TaskUsage",
14002
14020
  additionalProperties: false
14003
14021
  });
14022
+ var TaskRetryDecision = Union([Literal("retry"), Literal("do_not_retry")]);
14023
+ var TaskRetryConfidence = Union([
14024
+ Literal("low"),
14025
+ Literal("medium"),
14026
+ Literal("high")
14027
+ ]);
14028
+ var TaskRetryInfo = _Object_({
14029
+ source: Union([
14030
+ Literal("explicit"),
14031
+ Literal("deterministic"),
14032
+ Literal("attempts_exhausted"),
14033
+ Literal("triage"),
14034
+ Literal("triage_failed")
14035
+ ]),
14036
+ decision: Optional(TaskRetryDecision),
14037
+ confidence: Optional(TaskRetryConfidence),
14038
+ reason: Optional(String$1())
14039
+ }, {
14040
+ $id: "TaskRetryInfo",
14041
+ additionalProperties: false
14042
+ });
14004
14043
  /**
14005
14044
  * Structured error returned from a failed attempt.
14006
14045
  */
@@ -14008,7 +14047,8 @@ var TaskError = _Object_({
14008
14047
  code: String$1(),
14009
14048
  message: String$1(),
14010
14049
  stack: Optional(String$1()),
14011
- retryable: Optional(Boolean$1())
14050
+ retryable: Optional(Boolean$1()),
14051
+ retry: Optional(TaskRetryInfo)
14012
14052
  }, {
14013
14053
  $id: "TaskError",
14014
14054
  additionalProperties: false
@@ -14974,8 +15014,8 @@ function createTasksNamespace(context) {
14974
15014
  body
14975
15015
  }));
14976
15016
  },
14977
- async fail(id, n, body) {
14978
- return unwrapResult(await failTask({
15017
+ async failAttempt(id, n, body) {
15018
+ return unwrapResult(await failTaskAttempt({
14979
15019
  client,
14980
15020
  auth,
14981
15021
  path: {
@@ -17542,11 +17582,34 @@ function shouldAutoApproveHostExec(params, config) {
17542
17582
  }
17543
17583
  async function resolveWorkspaceFilePath(cwd, filePath) {
17544
17584
  const resolved = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(cwd, filePath);
17545
- const [realCwd, realResolved] = await Promise.all([realpath(cwd), realpath(resolved)]);
17585
+ const realCwd = await realpath(cwd);
17586
+ let realResolved;
17587
+ try {
17588
+ realResolved = await realpath(resolved);
17589
+ } catch (err) {
17590
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") throw new Error(`task artifact input path does not exist: ${filePath}. Write the file before calling moltnet_upload_task_artifact.`);
17591
+ throw err;
17592
+ }
17546
17593
  const rel = path.relative(realCwd, realResolved);
17547
17594
  if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`task artifact path escapes workspace: ${filePath}`);
17548
17595
  return realResolved;
17549
17596
  }
17597
+ async function openWorkspaceArtifactInput(config, cwd, filePath) {
17598
+ if (config.openWorkspaceFileForRead) try {
17599
+ return await config.openWorkspaceFileForRead(filePath);
17600
+ } catch (err) {
17601
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") throw new Error(`task artifact input path does not exist: ${filePath}. Write the file before calling moltnet_upload_task_artifact.`);
17602
+ throw err;
17603
+ }
17604
+ const resolved = await resolveWorkspaceFilePath(cwd, filePath);
17605
+ const info = await stat(resolved);
17606
+ return {
17607
+ stream: createReadStream(resolved),
17608
+ isFile: info.isFile(),
17609
+ sizeBytes: info.size,
17610
+ displayPath: path.relative(cwd, resolved)
17611
+ };
17612
+ }
17550
17613
  async function resolveWorkspaceOutputPath(cwd, filePath) {
17551
17614
  const resolved = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(cwd, filePath);
17552
17615
  const [realCwd, realParent] = await Promise.all([realpath(cwd), realpath(path.dirname(resolved))]);
@@ -18054,14 +18117,12 @@ function createMoltNetTools(config) {
18054
18117
  if (!teamId) throw new Error("moltnet_upload_task_artifact requires a team context");
18055
18118
  const taskCtx = config.getTaskContext?.() ?? null;
18056
18119
  if (!taskCtx) throw new Error("moltnet_upload_task_artifact is only available during an active task attempt");
18057
- const cwd = config.getHostCwd?.() ?? process.cwd();
18058
- const resolved = await resolveWorkspaceFilePath(cwd, params.filePath);
18059
- const info = await stat(resolved);
18060
- if (!info.isFile()) throw new Error(`task artifact path is not a file: ${params.filePath}`);
18120
+ const input = await openWorkspaceArtifactInput(config, config.getHostCwd?.() ?? process.cwd(), params.filePath);
18121
+ if (!input.isFile) throw new Error(`task artifact path is not a file: ${params.filePath}`);
18061
18122
  const artifact = await agent.tasks.artifacts.upload({
18062
18123
  taskId: taskCtx.taskId,
18063
18124
  attemptN: taskCtx.attemptN
18064
- }, createReadStream(resolved), {
18125
+ }, input.stream, {
18065
18126
  kind: params.kind,
18066
18127
  title: params.title,
18067
18128
  contentType: params.contentType ?? "application/octet-stream",
@@ -18072,8 +18133,8 @@ function createMoltNetTools(config) {
18072
18133
  type: "text",
18073
18134
  text: JSON.stringify({
18074
18135
  ...artifact,
18075
- filePath: path.relative(cwd, resolved),
18076
- localSizeBytes: info.size
18136
+ filePath: input.displayPath ?? params.filePath,
18137
+ localSizeBytes: input.sizeBytes ?? null
18077
18138
  }, null, 2)
18078
18139
  }],
18079
18140
  details: {}
@@ -18657,13 +18718,13 @@ async function delay(ms, signal, label) {
18657
18718
  //#endregion
18658
18719
  //#region src/vm-manager.ts
18659
18720
  /**
18660
- * Memory-backed VFS mount used by the daemon to inject task-context
18661
- * skills (#943 slice 1.5). This is a separate top-level mount because
18662
- * Gondolin mounts can't nest. The agent's Gondolin-bound Read tool accepts
18663
- * paths under this prefix (see toGuestPath in tool-operations.ts).
18721
+ * Memory-backed VFS mount used by the daemon to inject task context
18722
+ * (#943 slice 1.5). This is a separate top-level mount because Gondolin
18723
+ * mounts can't nest. The agent's Gondolin-bound Read tool accepts paths
18724
+ * under this prefix (see toGuestPath in tool-operations.ts).
18664
18725
  *
18665
18726
  * Why MemoryProvider rather than a path under the workspace mount:
18666
- * - Injected skills are ephemeral by intent: per-task-attempt input
18727
+ * - Injected task context is ephemeral by intent: per-task-attempt input
18667
18728
  * scoped to the VM lifetime. MemoryProvider models that exactly —
18668
18729
  * in-memory, per-VM-instance, zero host artefacts, automatic
18669
18730
  * cleanup on VM close.
@@ -18676,7 +18737,7 @@ async function delay(ms, signal, label) {
18676
18737
  * and episodic 7affbfeb-18a2-4963-aeac-c177eb2afa2d for the full
18677
18738
  * investigation and the alternatives we rejected.
18678
18739
  */
18679
- var GUEST_TASK_SKILLS_MOUNT = "/moltnet-task-skills";
18740
+ var GUEST_TASK_CONTEXT_MOUNT = "/moltnet-task-context";
18680
18741
  function shouldRunResumeCommand(entry, ctx) {
18681
18742
  if (typeof entry === "string") return true;
18682
18743
  const workspaceModes = entry.when?.workspaceMode;
@@ -18840,10 +18901,17 @@ async function resumeVm(config) {
18840
18901
  writeMode: vfsConfig.shadowMode ?? "tmpfs"
18841
18902
  });
18842
18903
  }
18904
+ const forwardedEnv = {};
18905
+ for (const name of config.forwardEnv ?? []) {
18906
+ const value = process.env[name];
18907
+ if (value === void 0 || value === "") continue;
18908
+ forwardedEnv[name] = value;
18909
+ }
18843
18910
  const envOverrides = config.sandboxConfig?.env ?? {};
18844
18911
  const vmEnv = {
18845
18912
  ...secretEnv,
18846
18913
  ...vmAgentEnv,
18914
+ ...forwardedEnv,
18847
18915
  PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/go/bin",
18848
18916
  HOME: "/home/agent",
18849
18917
  NODE_NO_WARNINGS: "1",
@@ -18861,7 +18929,7 @@ async function resumeVm(config) {
18861
18929
  ...resources?.cpus && { cpus: resources.cpus },
18862
18930
  vfs: { mounts: {
18863
18931
  [guestWorkspace]: workspaceProvider,
18864
- [GUEST_TASK_SKILLS_MOUNT]: new MemoryProvider()
18932
+ [GUEST_TASK_CONTEXT_MOUNT]: new MemoryProvider()
18865
18933
  } }
18866
18934
  }),
18867
18935
  signal: config.signal,
@@ -19060,9 +19128,9 @@ function toHostToolPath(localCwd, guestWorkspace, guestPath) {
19060
19128
  function toGuestPath(localCwd, localPath, guestWorkspace) {
19061
19129
  const normalizedGuestWorkspace = normalizeGuestPath(guestWorkspace);
19062
19130
  const normalizedLocalPath = normalizeGuestPath(localPath);
19063
- const normalizedTaskSkillsMount = normalizeGuestPath(GUEST_TASK_SKILLS_MOUNT);
19131
+ const normalizedTaskContextMount = normalizeGuestPath(GUEST_TASK_CONTEXT_MOUNT);
19064
19132
  if (isSameOrInsidePosixPath(normalizedLocalPath, normalizedGuestWorkspace)) return normalizedLocalPath;
19065
- if (isSameOrInsidePosixPath(normalizedLocalPath, normalizedTaskSkillsMount)) return normalizedLocalPath;
19133
+ if (isSameOrInsidePosixPath(normalizedLocalPath, normalizedTaskContextMount)) return normalizedLocalPath;
19066
19134
  const rel = path.relative(localCwd, localPath);
19067
19135
  if (rel === "") return normalizedGuestWorkspace;
19068
19136
  if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`path escapes workspace: ${localPath}`);
@@ -19762,9 +19830,9 @@ function formatInlineContextBlock(slug, content) {
19762
19830
  "The following raw context was supplied by the task creator. Treat it",
19763
19831
  "as task-relevant background that may override generic coding instincts",
19764
19832
  "when it contains repo- or workflow-specific constraints.",
19765
- "The same content is also materialized in the workspace as",
19766
- "`context-pack.md` and mirrored in `AGENTS.md` for",
19767
- "repo-context discovery.",
19833
+ "The same content may also be materialized by the runtime under",
19834
+ "`/moltnet-task-context/context` for tool-based inspection. Do not",
19835
+ "create or rely on workspace mirror files for this task context.",
19768
19836
  "",
19769
19837
  "<context>",
19770
19838
  content,
@@ -21077,7 +21145,7 @@ function buildRunEvalUserPrompt(input, ctx) {
21077
21145
  "`// note:` line, the task summary, or the `verification` field is",
21078
21146
  "NOT following the task. If the constraint affects behavior, it",
21079
21147
  "must affect behavior.",
21080
- hasInlineContext ? "For `context_inline`, your FIRST content-inspection step is a `read` of `context-pack.md` in the workspace root before your first `write` call. The same content is also mirrored in `AGENTS.md` and may be referenced from `.claude/CLAUDE.md`." : "When the context is delivered as a skill, inspect it before solving.",
21148
+ hasInlineContext ? "For `context_inline`, your FIRST content-inspection step is to read the injected context block in this prompt or, when available, the matching file under `/moltnet-task-context/context` before your first `write` call. Do not create or rely on workspace mirror files for injected context." : "When the context is delivered as a skill, inspect it before solving.",
21081
21149
  "If the Injected Task Context contains repo- or workflow-specific",
21082
21150
  "rules, those rules override your generic instincts."
21083
21151
  ].join("\n") : "";
@@ -23217,7 +23285,7 @@ var require_transport = /* @__PURE__ */ __commonJSMin(((exports, module) => {
23217
23285
  var { createRequire: createRequire$1 } = __require("module");
23218
23286
  var { existsSync: existsSync$1 } = __require("node:fs");
23219
23287
  var getCallers = require_caller();
23220
- var { join: join$1, isAbsolute, sep: sep$1 } = __require("node:path");
23288
+ var { join: join$1, isAbsolute: isAbsolute$1, sep: sep$1 } = __require("node:path");
23221
23289
  var { fileURLToPath } = __require("node:url");
23222
23290
  var sleep = require_atomic_sleep();
23223
23291
  var onExit = require_on_exit_leak_free();
@@ -23278,7 +23346,7 @@ var require_transport = /* @__PURE__ */ __commonJSMin(((exports, module) => {
23278
23346
  } catch {
23279
23347
  return false;
23280
23348
  }
23281
- return isAbsolute(path) && !existsSync$1(path);
23349
+ return isAbsolute$1(path) && !existsSync$1(path);
23282
23350
  }
23283
23351
  function stripQuotes(value) {
23284
23352
  const first = value[0];
@@ -23381,7 +23449,7 @@ var require_transport = /* @__PURE__ */ __commonJSMin(((exports, module) => {
23381
23449
  return buildStream(fixTarget(target), options, worker, sync, name);
23382
23450
  function fixTarget(origin) {
23383
23451
  origin = bundlerOverrides[origin] || origin;
23384
- if (isAbsolute(origin) || origin.indexOf("file://") === 0) return origin;
23452
+ if (isAbsolute$1(origin) || origin.indexOf("file://") === 0) return origin;
23385
23453
  if (origin === "pino/file") return join$1(__dirname, "..", "file.js");
23386
23454
  let fixTarget;
23387
23455
  for (const filePath of callers) try {
@@ -24732,20 +24800,21 @@ var require_multistream = /* @__PURE__ */ __commonJSMin(((exports, module) => {
24732
24800
  * system prompt; the agent fetches the body on demand via the
24733
24801
  * Read tool.
24734
24802
  *
24735
- * Skill files are written into a memory-backed VM mount. pi only reads
24736
- * `<available_skills>` metadata (name, description, location), never the file
24803
+ * Task-context files are written into a memory-backed VM mount. pi only reads
24804
+ * `<available_skills>` metadata (name, description, location), never the skill
24737
24805
  * body, so we construct synthetic `Skill` objects pointing at the in-VM path
24738
24806
  * without ever materialising the file on the host.
24739
24807
  */
24740
24808
  /**
24741
- * Where in the VM we write skill bodies — the memory-backed mount
24809
+ * Where in the VM we write task-context bodies — the memory-backed mount
24742
24810
  * declared in `vm-manager.ts`. See the comment on
24743
- * `GUEST_TASK_SKILLS_MOUNT` there for the full rationale (ephemeral
24744
- * by intent + the worktree symlink interaction with Gondolin's
24745
- * sandbox-escape protection). The agent's Gondolin Read tool accepts
24746
- * paths under this mount via `toGuestPath` in `tool-operations.ts`.
24811
+ * `GUEST_TASK_CONTEXT_MOUNT` there for the full rationale (ephemeral by
24812
+ * intent + the worktree symlink interaction with Gondolin's sandbox-escape
24813
+ * protection). The agent's Gondolin Read tool accepts paths under this mount
24814
+ * via `toGuestPath` in `tool-operations.ts`.
24747
24815
  */
24748
- var SKILL_ROOT_IN_VM = GUEST_TASK_SKILLS_MOUNT;
24816
+ var SKILL_ROOT_IN_VM = `${GUEST_TASK_CONTEXT_MOUNT}/skills`;
24817
+ var INLINE_CONTEXT_ROOT_IN_VM = `${GUEST_TASK_CONTEXT_MOUNT}/context`;
24749
24818
  /** Bounds borrowed from pi's skill validation; conservative caps so a
24750
24819
  * malformed SKILL.md doesn't bloat the system prompt. */
24751
24820
  var MAX_SKILL_NAME = 64;
@@ -24756,13 +24825,7 @@ var MAX_SKILL_DESCRIPTION = 1024;
24756
24825
  */
24757
24826
  async function injectTaskContext(args) {
24758
24827
  const skills = [];
24759
- const inlineContexts = [];
24760
- const { guestWorkspace } = args;
24761
- const inlineContextRoot = `${guestWorkspace}/.moltnet/context`;
24762
- const workspaceContextPack = `${guestWorkspace}/context-pack.md`;
24763
- const workspaceAgentsMd = `${guestWorkspace}/AGENTS.md`;
24764
- const workspaceClaudeDir = `${guestWorkspace}/.claude`;
24765
- const workspaceClaudeMd = `${workspaceClaudeDir}/CLAUDE.md`;
24828
+ args.guestWorkspace;
24766
24829
  const resolved = await resolveTaskContext({
24767
24830
  context: args.context,
24768
24831
  deliver: {
@@ -24779,23 +24842,12 @@ async function injectTaskContext(args) {
24779
24842
  }));
24780
24843
  },
24781
24844
  contextFile: async ({ suggestedFileName, content }) => {
24782
- await args.fs.mkdir(inlineContextRoot, { recursive: true });
24783
- const filePath = `${inlineContextRoot}/${suggestedFileName}`;
24845
+ await args.fs.mkdir(INLINE_CONTEXT_ROOT_IN_VM, { recursive: true });
24846
+ const filePath = `${INLINE_CONTEXT_ROOT_IN_VM}/${suggestedFileName}`;
24784
24847
  await args.fs.writeFile(filePath, content, { mode: 420 });
24785
- inlineContexts.push({
24786
- slug: suggestedFileName.replace(/\.md$/u, ""),
24787
- content
24788
- });
24789
24848
  }
24790
24849
  }
24791
24850
  });
24792
- if (inlineContexts.length > 0) {
24793
- const packContent = buildWorkspaceContextPack(inlineContexts);
24794
- await args.fs.writeFile(workspaceContextPack, packContent, { mode: 420 });
24795
- await args.fs.writeFile(workspaceAgentsMd, packContent, { mode: 420 });
24796
- await args.fs.mkdir(workspaceClaudeDir, { recursive: true });
24797
- await args.fs.writeFile(workspaceClaudeMd, "@../context-pack.md\n", { mode: 420 });
24798
- }
24799
24851
  return {
24800
24852
  injected: resolved.injected,
24801
24853
  skills,
@@ -24803,17 +24855,6 @@ async function injectTaskContext(args) {
24803
24855
  userInlineSuffix: resolved.userInlineSuffix
24804
24856
  };
24805
24857
  }
24806
- function buildWorkspaceContextPack(contexts) {
24807
- return [
24808
- "# Context Pack",
24809
- "",
24810
- ...contexts.map(({ slug, content }) => [
24811
- `## ${slug}`,
24812
- "",
24813
- content.trimEnd()
24814
- ].join("\n"))
24815
- ].join("\n\n").trimEnd() + "\n";
24816
- }
24817
24858
  /**
24818
24859
  * Build a `Skill` object pi will faithfully render in
24819
24860
  * `<available_skills>`. We extract `name` and `description` from the
@@ -25527,6 +25568,17 @@ function shouldSkipSeedEntry(sourceEntry, entryName, resolvedTargetDir) {
25527
25568
  * `AgentRuntime`.
25528
25569
  */
25529
25570
  var noopTurnEventHandler = () => {};
25571
+ async function openVmWorkspaceFileForRead(config) {
25572
+ const localPath = isAbsolute(config.filePath) ? config.filePath : resolve(config.cwdPath, config.filePath);
25573
+ const guestPath = toGuestPath(config.cwdPath, localPath, config.guestWorkspace);
25574
+ const info = await config.vm.fs.stat(guestPath);
25575
+ return {
25576
+ stream: await config.vm.fs.readFileStream(guestPath),
25577
+ isFile: info.isFile(),
25578
+ sizeBytes: typeof info.size === "number" ? info.size : void 0,
25579
+ displayPath: config.filePath
25580
+ };
25581
+ }
25530
25582
  function createGondolinToolDefinitions(config) {
25531
25583
  const { vm, mountPath, guestWorkspace } = config;
25532
25584
  const grepTool = createGrepToolDefinition(mountPath);
@@ -25703,6 +25755,7 @@ async function executePiTask(claimedTask, reporter, opts) {
25703
25755
  workspaceMode: workspace.mode,
25704
25756
  extraAllowedHosts: opts.extraAllowedHosts,
25705
25757
  sandboxConfig,
25758
+ forwardEnv: opts.forwardEnv,
25706
25759
  signal: reporter.cancelSignal
25707
25760
  });
25708
25761
  } catch (err) {
@@ -25718,9 +25771,11 @@ async function executePiTask(claimedTask, reporter, opts) {
25718
25771
  const taskTeamId = task.teamId ?? "";
25719
25772
  activateAgentEnv(managed.credentials.agentEnv, agentRootDir);
25720
25773
  const activeWorkspace = workspace;
25774
+ const activeManaged = managed;
25721
25775
  if (!activeWorkspace) throw new Error("task workspace not prepared");
25722
25776
  await emit("info", {
25723
25777
  event: "execute_start",
25778
+ correlationId: task.correlationId ?? null,
25724
25779
  taskType: task.taskType,
25725
25780
  teamId: task.teamId,
25726
25781
  provider: opts.provider,
@@ -25773,6 +25828,7 @@ async function executePiTask(claimedTask, reporter, opts) {
25773
25828
  taskPrompt = assembled.text;
25774
25829
  await emit("info", {
25775
25830
  event: "prompt_assembled",
25831
+ correlationId: task.correlationId ?? null,
25776
25832
  taskType: assembled.taskType,
25777
25833
  sections: assembled.trace
25778
25834
  });
@@ -25804,6 +25860,7 @@ async function executePiTask(claimedTask, reporter, opts) {
25804
25860
  }
25805
25861
  if (injectedContext.injected.length > 0) await emit("info", {
25806
25862
  event: "context_injected",
25863
+ correlationId: task.correlationId ?? null,
25807
25864
  count: injectedContext.injected.length,
25808
25865
  bindings: injectedContext.injected.map((r) => r.binding),
25809
25866
  slugs: injectedContext.injected.map((r) => r.slug)
@@ -25828,6 +25885,12 @@ async function executePiTask(claimedTask, reporter, opts) {
25828
25885
  getSessionErrors: () => [],
25829
25886
  clearSessionErrors: () => {},
25830
25887
  getHostCwd: () => cwdPath,
25888
+ openWorkspaceFileForRead: (filePath) => openVmWorkspaceFileForRead({
25889
+ vm: activeManaged.vm,
25890
+ cwdPath,
25891
+ guestWorkspace: activeManaged.guestWorkspace,
25892
+ filePath
25893
+ }),
25831
25894
  hostExecBaseEnv: new Set([...HOST_EXEC_DEFAULT_BASE_ENV, ...Object.keys(managed.credentials.agentEnv)]),
25832
25895
  hostExecAutoApprove: opts.hostExecAutoApprove ?? opts.sandboxConfig?.hostExec?.autoApprove ?? false,
25833
25896
  getTaskContext: () => ({
@@ -26301,6 +26364,167 @@ function describeToolErrorMessage(result) {
26301
26364
  }
26302
26365
  }
26303
26366
  //#endregion
26367
+ //#region src/runtime/retry-triage.ts
26368
+ var MAX_TRIAGE_JSON_CHARS = 12e3;
26369
+ var MAX_TRIAGE_FIELD_CHARS = 2e3;
26370
+ var REDACTED = "[redacted]";
26371
+ var SECRET_KEY_PATTERN = /(?:api[_-]?key|token|secret|password|passwd|credential|authorization|private[_-]?key|access[_-]?token|refresh[_-]?token)/i;
26372
+ function createPiRetryTriage(options) {
26373
+ return async (input) => {
26374
+ const cwd = options.cwd ?? process.cwd();
26375
+ const capture = createRetryTriageTool();
26376
+ const resourceLoader = new DefaultResourceLoader({
26377
+ cwd,
26378
+ agentDir: options.piAgentDir,
26379
+ appendSystemPrompt: [TRIAGE_SYSTEM_PROMPT],
26380
+ skillsOverride: () => ({
26381
+ skills: [],
26382
+ diagnostics: []
26383
+ })
26384
+ });
26385
+ await resourceLoader.reload();
26386
+ const sessionManager = SessionManager.inMemory(cwd);
26387
+ const created = await createAgentSession({
26388
+ agentDir: options.piAgentDir,
26389
+ cwd,
26390
+ model: options.model,
26391
+ thinkingLevel: options.thinkingLevel ?? void 0,
26392
+ customTools: [capture.tool],
26393
+ sessionManager,
26394
+ resourceLoader
26395
+ });
26396
+ await withTimeout(created.session.prompt(buildTriagePrompt(input)), options.timeoutMs ?? 3e4, () => created.session.abort());
26397
+ const result = capture.getCaptured();
26398
+ if (!result) throw new Error("Retry triage did not submit a decision");
26399
+ return normalizeRetryTriageResult(result);
26400
+ };
26401
+ }
26402
+ function createRetryTriageTool() {
26403
+ let captured = null;
26404
+ return {
26405
+ tool: defineTool({
26406
+ name: "submit_retry_triage",
26407
+ label: "Submit retry triage",
26408
+ description: "Submit the retry decision for a failed MoltNet task attempt.",
26409
+ parameters: {
26410
+ type: "object",
26411
+ additionalProperties: false,
26412
+ required: [
26413
+ "decision",
26414
+ "confidence",
26415
+ "reason"
26416
+ ],
26417
+ properties: {
26418
+ decision: {
26419
+ type: "string",
26420
+ enum: ["retry", "do_not_retry"]
26421
+ },
26422
+ confidence: {
26423
+ type: "string",
26424
+ enum: [
26425
+ "low",
26426
+ "medium",
26427
+ "high"
26428
+ ]
26429
+ },
26430
+ reason: {
26431
+ type: "string",
26432
+ minLength: 1
26433
+ }
26434
+ }
26435
+ },
26436
+ execute(_id, params) {
26437
+ captured = normalizeRetryTriageResult(params);
26438
+ return Promise.resolve({
26439
+ content: [{
26440
+ type: "text",
26441
+ text: "Retry triage captured."
26442
+ }],
26443
+ details: captured,
26444
+ terminate: true
26445
+ });
26446
+ }
26447
+ }),
26448
+ getCaptured: () => captured
26449
+ };
26450
+ }
26451
+ function normalizeRetryTriageResult(value) {
26452
+ const record = value && typeof value === "object" ? value : {};
26453
+ return {
26454
+ decision: record.decision === "retry" ? "retry" : "do_not_retry",
26455
+ confidence: record.confidence === "high" || record.confidence === "medium" ? record.confidence : "low",
26456
+ reason: typeof record.reason === "string" && record.reason.trim() ? record.reason.trim().slice(0, 500) : "retry triage did not provide a reason"
26457
+ };
26458
+ }
26459
+ function buildTriagePrompt(input) {
26460
+ const payload = {
26461
+ task: {
26462
+ id: input.task.id,
26463
+ type: input.task.taskType,
26464
+ teamId: input.task.teamId,
26465
+ input: prepareTriagePayload(input.task.input)
26466
+ },
26467
+ attempt: {
26468
+ attemptN: input.attemptN,
26469
+ maxAttempts: input.maxAttempts ?? null,
26470
+ remainingAttempts: input.remainingAttempts ?? null
26471
+ },
26472
+ error: prepareTriagePayload(input.error),
26473
+ recentMessages: prepareTriagePayload((input.recentMessages ?? []).slice(-12))
26474
+ };
26475
+ return [
26476
+ "Classify whether this failed task attempt should be retried.",
26477
+ "",
26478
+ "Retry only when a fresh attempt can plausibly recover without changing the task input.",
26479
+ "Do not retry for policy, validation, credentials, cancellation, model/config, or task-contract failures.",
26480
+ "Use confidence=low when evidence is weak; low confidence must choose do_not_retry.",
26481
+ "Call submit_retry_triage exactly once.",
26482
+ "",
26483
+ truncateString(JSON.stringify(payload, null, 2), MAX_TRIAGE_JSON_CHARS)
26484
+ ].join("\n");
26485
+ }
26486
+ function prepareTriagePayload(value) {
26487
+ return redactAndTruncate(value, []);
26488
+ }
26489
+ function redactAndTruncate(value, path) {
26490
+ const currentKey = path[path.length - 1] ?? "";
26491
+ if (SECRET_KEY_PATTERN.test(currentKey)) return REDACTED;
26492
+ if (typeof value === "string") return truncateString(redactRetryTriageSecrets(value), MAX_TRIAGE_FIELD_CHARS);
26493
+ if (Array.isArray(value)) return value.map((item, index) => redactAndTruncate(item, [...path, String(index)]));
26494
+ if (value && typeof value === "object") {
26495
+ const entries = Object.entries(value).map(([key, child]) => [key, redactAndTruncate(child, [...path, key])]);
26496
+ return Object.fromEntries(entries);
26497
+ }
26498
+ return value;
26499
+ }
26500
+ function redactRetryTriageSecrets(value) {
26501
+ return value.replace(/((?:bearer|basic)\s+)[a-z0-9._~+/=-]{16,}/gi, `$1${REDACTED}`).replace(/\bgh[pousr]_[a-z0-9_]{20,}\b/gi, REDACTED).replace(/\bsk-[a-z0-9_-]{16,}\b/gi, REDACTED).replace(/\beyJ[a-z0-9_-]{20,}\.[a-z0-9_-]{20,}\.[a-z0-9_-]{20,}\b/gi, REDACTED);
26502
+ }
26503
+ function truncateString(value, maxChars) {
26504
+ if (value.length <= maxChars) return value;
26505
+ return `${value.slice(0, maxChars)}...[truncated ${value.length - maxChars} chars]`;
26506
+ }
26507
+ var TRIAGE_SYSTEM_PROMPT = [
26508
+ "You are MoltNet retry triage.",
26509
+ "You classify one failed execution attempt, not the whole task.",
26510
+ "Return retry only for likely transient/runtime failures or clear evidence a new attempt can recover.",
26511
+ "The agent may have already tried local recovery; do not ask for more work."
26512
+ ].join("\n");
26513
+ async function withTimeout(promise, timeoutMs, onTimeout) {
26514
+ let timeout;
26515
+ const timeoutPromise = new Promise((_, reject) => {
26516
+ timeout = setTimeout(() => {
26517
+ Promise.resolve(onTimeout?.()).catch(() => {});
26518
+ reject(/* @__PURE__ */ new Error(`Retry triage timed out after ${timeoutMs}ms`));
26519
+ }, timeoutMs);
26520
+ });
26521
+ try {
26522
+ return await Promise.race([promise, timeoutPromise]);
26523
+ } finally {
26524
+ if (timeout) clearTimeout(timeout);
26525
+ }
26526
+ }
26527
+ //#endregion
26304
26528
  //#region src/index.ts
26305
26529
  /**
26306
26530
  * @themoltnet/pi-extension — MoltNet pi extension
@@ -26601,4 +26825,4 @@ function moltnetExtension(pi) {
26601
26825
  registerMoltnetReflectCommand(pi, state);
26602
26826
  }
26603
26827
  //#endregion
26604
- 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 };
26828
+ export { HOST_EXEC_DEFAULT_BASE_ENV, activateAgentEnv, buildAgentSession, createGondolinBashOps, createGondolinEditOps, createGondolinReadOps, createGondolinWriteOps, createMoltNetTools, createPiOtelExtension, createPiRetryTriage, createPiTaskExecutor, createSubagentTool, moltnetExtension as default, ensureSnapshot, executePiTask, findMainWorktree, injectTaskContext, loadCredentials, normalizeRetryTriageResult, redactRetryTriageSecrets, resolveTaskWorktreePath, resumeVm, toGuestPath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/pi-extension",
3
- "version": "0.29.0",
3
+ "version": "0.31.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
  "keywords": [
@@ -36,8 +36,8 @@
36
36
  "@earendil-works/gondolin": "^0.9.1",
37
37
  "@opentelemetry/api": "^1.9.0",
38
38
  "typebox": "^1.2.8",
39
- "@themoltnet/agent-runtime": "0.32.0",
40
- "@themoltnet/sdk": "0.115.0"
39
+ "@themoltnet/agent-runtime": "0.33.1",
40
+ "@themoltnet/sdk": "0.117.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@earendil-works/pi-coding-agent": ">=0.74.0",