@themoltnet/pi-extension 0.30.0 → 0.31.1

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
@@ -216,8 +216,9 @@ Shell commands that run on every VM resume, after platform setup and before the
216
216
  agent session starts.
217
217
 
218
218
  Use this for per-session bootstrap that should not invalidate the snapshot
219
- cache: mounting tmpfs, warming package-manager state, lightweight repo-local
220
- setup.
219
+ cache: enabling package-manager shims or lightweight repo-local setup. Keep
220
+ network-heavy dependency fetches out of the default TUI startup path; run them
221
+ explicitly when a daemon/provisioning flow wants to prewarm a store.
221
222
 
222
223
  Important properties:
223
224
 
@@ -284,16 +285,22 @@ Dogfood trail:
284
285
  - `17f0ac6f-07f0-4e12-b5e5-d35a0fa2df6c` — first 100x pnpm recipe
285
286
  - `2e4e25a9-ef4b-46bf-a55d-6c2b1159ee61` — follow-up fix for per-workspace `node_modules`
286
287
 
287
- `vfs.shadow: ["node_modules"]` is still useful to hide host-built artifacts,
288
- but it does not solve the hot-path problem by itself. For fast pnpm setup, move
289
- both endpoints off the FUSE bridge:
288
+ The extension now always shadows any `node_modules` path into VM-local
289
+ executable storage. That is VFS policy rather than a resume command, so it also
290
+ covers worktrees the live agent creates after resume. Package-manager stores
291
+ are different: pnpm's content-addressed store should remain a reusable
292
+ guest-local directory, e.g. `NPM_CONFIG_STORE_DIR=/opt/pnpm-store`, not tmpfs.
293
+ Runtime profiles or `sandbox.json` should avoid network-heavy fetch/install
294
+ steps during the default TUI resume path. If a daemon/provisioning flow needs
295
+ fast first installs, prewarm the store explicitly with `pnpm fetch` after the
296
+ sandbox is available.
290
297
 
291
- - package store on guest-local disk, e.g. `NPM_CONFIG_STORE_DIR=/opt/pnpm-store`
292
- - install target on guest tmpfs via `resumeCommands`
298
+ Run the real VM integration check locally with:
293
299
 
294
- Current themoltnet `sandbox.json` does this by mounting tmpfs over the root and
295
- per-workspace `node_modules` directories before running `pnpm install
296
- --frozen-lockfile`.
300
+ ```bash
301
+ MOLTNET_PI_VM_INTEGRATION=1 \
302
+ pnpm exec nx run @themoltnet/pi-extension:test-ci--src/vm-manager.integration.test.ts
303
+ ```
297
304
 
298
305
  ### `env`
299
306
 
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ import { BashOperations } from '@earendil-works/pi-coding-agent';
4
4
  import { connect } from '@themoltnet/sdk';
5
5
  import { EditOperations } from '@earendil-works/pi-coding-agent';
6
6
  import { ExtensionAPI } from '@earendil-works/pi-coding-agent';
7
+ import { ExtensionContext } from '@earendil-works/pi-coding-agent';
7
8
  import { LoadSkillsResult } from '@earendil-works/pi-coding-agent';
8
9
  import { Model } from '@earendil-works/pi-ai';
9
10
  import { Readable } from 'node:stream';
@@ -145,6 +146,16 @@ export declare function createMoltNetTools(config: MoltNetToolsConfig): ToolDefi
145
146
 
146
147
  export declare function createPiOtelExtension(options?: PiOtelOptions): (pi: ExtensionAPI) => void;
147
148
 
149
+ export declare function createPiProviderErrorRetryUi(ctx: ExtensionContext): ProviderErrorRetryUi | undefined;
150
+
151
+ export declare function createPiRetryTriage(options: {
152
+ model: Model<Api>;
153
+ thinkingLevel?: PiRetryTriageThinkingLevel | null;
154
+ piAgentDir: string;
155
+ timeoutMs?: number;
156
+ cwd?: string;
157
+ }): PiRetryTriage;
158
+
148
159
  /**
149
160
  * Factory that builds a pi-specific `executeTask` function suitable for
150
161
  * injection into `AgentRuntime`. The returned function caches the resolved
@@ -365,6 +376,34 @@ export declare interface ExecutePiTaskOptions {
365
376
  * Default `3`. Set to `0` to disable. Closes part of #1094.
366
377
  */
367
378
  maxBashTimeouts?: number;
379
+ /**
380
+ * Number of correction turns allowed after the first invalid submit-output
381
+ * tool call. A value of 2 permits three invalid submit calls total before
382
+ * the attempt fails with output_validation_failed.
383
+ */
384
+ maxSubmitValidationRetries?: number;
385
+ /**
386
+ * Cap provider-error retries inside the same Pi session. A retry is attempted
387
+ * only after a Pi assistant turn ends with `stopReason: "error"` and the
388
+ * provider diagnostic is not a known credential/model/config failure. This is
389
+ * distinct from daemon attempt retry: the active session keeps its context and
390
+ * receives a short continuation prompt.
391
+ *
392
+ * Default `2`. Set to `0` to disable.
393
+ */
394
+ maxProviderErrorRetries?: number;
395
+ /** Base delay for same-session provider-error retries. Default `2000`. */
396
+ providerErrorRetryBaseDelayMs?: number;
397
+ /** Maximum delay for same-session provider-error retries. Default `30000`. */
398
+ providerErrorRetryMaxDelayMs?: number;
399
+ /** Continuation prompt sent after a retryable provider error. Default `Go on`. */
400
+ providerErrorRetryPrompt?: string;
401
+ /**
402
+ * Optional UI adapter for interactive pi/TUI callers. The daemon normally
403
+ * leaves this unset and consumes the structured `provider_error_retry` task
404
+ * message instead.
405
+ */
406
+ providerErrorRetryUi?: ProviderErrorRetryUi;
368
407
  /**
369
408
  * Skip per-call UI approval for matching `moltnet_host_exec` commands.
370
409
  * Keep false/undefined for interactive consumers. `true` skips every dialog
@@ -524,6 +563,8 @@ declare interface MoltNetToolsConfig {
524
563
  getTaskContext?(): MoltNetTaskContext | null;
525
564
  }
526
565
 
566
+ export declare function normalizeRetryTriageResult(value: unknown): PiRetryTriageResult;
567
+
527
568
  export declare interface PiOtelOptions {
528
569
  /** Agent name for `gen_ai.agent.name` on the root span. */
529
570
  agentName?: string;
@@ -535,6 +576,38 @@ export declare interface PiOtelOptions {
535
576
  spanAttributes?: Record<string, string | number | boolean>;
536
577
  }
537
578
 
579
+ export declare type PiRetryTriage = (input: PiRetryTriageInput) => Promise<PiRetryTriageResult>;
580
+
581
+ export declare type PiRetryTriageConfidence = RetryTriageConfidence;
582
+
583
+ export declare type PiRetryTriageDecision = RetryTriageDecision;
584
+
585
+ export declare interface PiRetryTriageInput {
586
+ task: {
587
+ id: string;
588
+ taskType: string;
589
+ teamId: string;
590
+ input: unknown;
591
+ };
592
+ attemptN: number;
593
+ maxAttempts?: number | null;
594
+ remainingAttempts?: number | null;
595
+ error: unknown;
596
+ recentMessages?: {
597
+ timestamp: string;
598
+ kind: string;
599
+ payload: unknown;
600
+ }[];
601
+ }
602
+
603
+ export declare interface PiRetryTriageResult {
604
+ decision: RetryTriageDecision;
605
+ confidence: RetryTriageConfidence;
606
+ reason: string;
607
+ }
608
+
609
+ export declare type PiRetryTriageThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
610
+
538
611
  export declare interface PiSessionPersistencePlan {
539
612
  sessionDir: string;
540
613
  forkFromSessionPath?: string | null;
@@ -605,6 +678,28 @@ declare interface PiWorkspaceSeedPlan {
605
678
  source: 'producer';
606
679
  }
607
680
 
681
+ export declare interface ProviderErrorRetryEvent extends Record<string, unknown> {
682
+ event: 'provider_error_retry';
683
+ retry: number;
684
+ maxRetries: number;
685
+ delayMs: number;
686
+ reason: string;
687
+ }
688
+
689
+ export declare type ProviderErrorRetryLevel = 'info' | 'warning' | 'error';
690
+
691
+ export declare interface ProviderErrorRetryUi {
692
+ /**
693
+ * Mirrors pi's `ctx.hasUI`. Undefined means "UI adapter is present"; false
694
+ * lets callers pass a stable adapter from both TUI and headless contexts.
695
+ */
696
+ hasUI?: boolean;
697
+ setStatus?: (key: string, message: string) => void | Promise<void>;
698
+ notify?: (message: string, level: ProviderErrorRetryLevel) => void | Promise<void>;
699
+ }
700
+
701
+ export declare function redactRetryTriageSecrets(value: string): string;
702
+
608
703
  export declare function resolveTaskWorktreePath(mainRepo: string, workspaceId: string): string;
609
704
 
610
705
  export declare interface ResumeCommand {
@@ -635,6 +730,10 @@ declare interface ResumeCommandWhen {
635
730
  */
636
731
  export declare function resumeVm(config: VmConfig): Promise<ManagedVm>;
637
732
 
733
+ export declare type RetryTriageConfidence = 'low' | 'medium' | 'high';
734
+
735
+ export declare type RetryTriageDecision = 'retry' | 'do_not_retry';
736
+
638
737
  export declare interface SandboxConfig {
639
738
  /** Snapshot build settings. */
640
739
  snapshot?: {
@@ -853,6 +952,12 @@ declare const TaskOutput: Type.TObject<{
853
952
  message: Type.TString;
854
953
  stack: Type.TOptional<Type.TString>;
855
954
  retryable: Type.TOptional<Type.TBoolean>;
955
+ retry: Type.TOptional<Type.TObject<{
956
+ source: Type.TUnion<[Type.TLiteral<"explicit">, Type.TLiteral<"deterministic">, Type.TLiteral<"attempts_exhausted">, Type.TLiteral<"triage">, Type.TLiteral<"triage_failed">]>;
957
+ decision: Type.TOptional<Type.TUnion<[Type.TLiteral<"retry">, Type.TLiteral<"do_not_retry">]>>;
958
+ confidence: Type.TOptional<Type.TUnion<[Type.TLiteral<"low">, Type.TLiteral<"medium">, Type.TLiteral<"high">]>>;
959
+ reason: Type.TOptional<Type.TString>;
960
+ }>>;
856
961
  }>>;
857
962
  contentSignature: Type.TOptional<Type.TString>;
858
963
  }>;
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { readFile, realpath, stat } from "node:fs/promises";
10
10
  import { homedir } from "node:os";
11
11
  import { pipeline } from "node:stream/promises";
12
12
  import { Type, getModel } from "@earendil-works/pi-ai";
13
- import { MemoryProvider, RealFSProvider, ShadowProvider, VM, VmCheckpoint, createHttpHooks, createShadowPathPredicate, ensureImageSelector, loadGuestAssets } from "@earendil-works/gondolin";
13
+ import { MemoryProvider, RealFSProvider, ShadowProvider, VM, VmCheckpoint, createHttpHooks, createShadowPathPredicate, ensureImageSelector, isWriteFlag, loadGuestAssets } from "@earendil-works/gondolin";
14
14
  import { parseEnv } from "node:util";
15
15
  import { SpanStatusCode, context, metrics, trace } from "@opentelemetry/api";
16
16
  //#region \0rolldown/runtime.js
@@ -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",
@@ -14019,6 +14019,27 @@ var TaskUsage = _Object_({
14019
14019
  $id: "TaskUsage",
14020
14020
  additionalProperties: false
14021
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
+ });
14022
14043
  /**
14023
14044
  * Structured error returned from a failed attempt.
14024
14045
  */
@@ -14026,7 +14047,8 @@ var TaskError = _Object_({
14026
14047
  code: String$1(),
14027
14048
  message: String$1(),
14028
14049
  stack: Optional(String$1()),
14029
- retryable: Optional(Boolean$1())
14050
+ retryable: Optional(Boolean$1()),
14051
+ retry: Optional(TaskRetryInfo)
14030
14052
  }, {
14031
14053
  $id: "TaskError",
14032
14054
  additionalProperties: false
@@ -14157,7 +14179,7 @@ _Object_({
14157
14179
  * @param errors - Field-level errors from `@moltnet/tasks` validators.
14158
14180
  * @returns One line per error, joined by newlines.
14159
14181
  */
14160
- function formatValidationErrors(errors) {
14182
+ function formatValidationErrors$1(errors) {
14161
14183
  return errors.map((e) => `${e.field}: ${e.message}`).join("\n");
14162
14184
  }
14163
14185
  /**
@@ -14170,7 +14192,7 @@ var TaskBuildError = class extends Error {
14170
14192
  /** The field-level validation errors that caused the build to fail. */
14171
14193
  errors;
14172
14194
  constructor(errors) {
14173
- super(`Task build failed:\n${formatValidationErrors(errors)}`);
14195
+ super(`Task build failed:\n${formatValidationErrors$1(errors)}`);
14174
14196
  this.name = "TaskBuildError";
14175
14197
  this.errors = errors;
14176
14198
  }
@@ -14185,7 +14207,7 @@ var TaskResultError = class extends Error {
14185
14207
  /** The field-level errors describing why the result could not be read. */
14186
14208
  errors;
14187
14209
  constructor(errors) {
14188
- super(`Task result error:\n${formatValidationErrors(errors)}`);
14210
+ super(`Task result error:\n${formatValidationErrors$1(errors)}`);
14189
14211
  this.name = "TaskResultError";
14190
14212
  this.errors = errors;
14191
14213
  }
@@ -14992,8 +15014,8 @@ function createTasksNamespace(context) {
14992
15014
  body
14993
15015
  }));
14994
15016
  },
14995
- async fail(id, n, body) {
14996
- return unwrapResult(await failTask({
15017
+ async failAttempt(id, n, body) {
15018
+ return unwrapResult(await failTaskAttempt({
14997
15019
  client,
14998
15020
  auth,
14999
15021
  path: {
@@ -18722,6 +18744,37 @@ function shouldRunResumeCommand(entry, ctx) {
18722
18744
  if (workspaceModes && !workspaceModes.includes(ctx.workspaceMode)) return false;
18723
18745
  return true;
18724
18746
  }
18747
+ function shouldShadowNodeModulesPath(pathname) {
18748
+ const normalized = path.posix.normalize(pathname);
18749
+ return normalized === "/node_modules" || normalized.startsWith("/node_modules/") || normalized.endsWith("/node_modules") || normalized.includes("/node_modules/");
18750
+ }
18751
+ function isNodeModulesBinPath(pathname) {
18752
+ const normalized = path.posix.normalize(pathname);
18753
+ return normalized.includes("/node_modules/.bin/") || normalized.startsWith("/node_modules/.bin/");
18754
+ }
18755
+ var AutoParentMemoryProvider = class extends MemoryProvider {
18756
+ ensureParentDir(pathname) {
18757
+ const parent = path.posix.dirname(path.posix.normalize(pathname));
18758
+ if (!parent || parent === "/" || parent === ".") return;
18759
+ this.mkdirSync(parent, { recursive: true });
18760
+ }
18761
+ async mkdir(pathname, options) {
18762
+ this.ensureParentDir(pathname);
18763
+ return super.mkdir(pathname, options);
18764
+ }
18765
+ mkdirSync(pathname, options) {
18766
+ this.ensureParentDir(pathname);
18767
+ return super.mkdirSync(pathname, options);
18768
+ }
18769
+ async open(pathname, flags, mode) {
18770
+ if (isWriteFlag(flags)) this.ensureParentDir(pathname);
18771
+ return super.open(pathname, flags, isWriteFlag(flags) && isNodeModulesBinPath(pathname) ? (mode ?? 493) | 73 : mode);
18772
+ }
18773
+ openSync(pathname, flags, mode) {
18774
+ if (isWriteFlag(flags)) this.ensureParentDir(pathname);
18775
+ return super.openSync(pathname, flags, isWriteFlag(flags) && isNodeModulesBinPath(pathname) ? (mode ?? 493) | 73 : mode);
18776
+ }
18777
+ };
18725
18778
  /**
18726
18779
  * Resolve the main worktree root (where .moltnet/ lives — it's untracked,
18727
18780
  * only exists in the main worktree, not in git worktrees).
@@ -18872,6 +18925,12 @@ async function resumeVm(config) {
18872
18925
  vmAgentEnv.MOLTNET_CREDENTIALS_PATH = `${vmAgentDir}/moltnet.json`;
18873
18926
  const vfsConfig = config.sandboxConfig?.vfs;
18874
18927
  let workspaceProvider = new RealFSProvider(config.mountPath);
18928
+ workspaceProvider = new ShadowProvider(workspaceProvider, {
18929
+ shouldShadow: ({ path: shadowPath }) => shouldShadowNodeModulesPath(shadowPath),
18930
+ denySymlinkBypass: false,
18931
+ tmpfs: new AutoParentMemoryProvider(),
18932
+ writeMode: "tmpfs"
18933
+ });
18875
18934
  if (vfsConfig?.shadow?.length) {
18876
18935
  const predicate = createShadowPathPredicate(vfsConfig.shadow);
18877
18936
  workspaceProvider = new ShadowProvider(workspaceProvider, {
@@ -19610,17 +19669,17 @@ function createPiModelOptionsExtension(options) {
19610
19669
  };
19611
19670
  }
19612
19671
  function applyPiModelOptions(payload, options) {
19613
- if (!isRecord(payload)) return void 0;
19672
+ if (!isRecord$1(payload)) return void 0;
19614
19673
  if (!hasPiModelOptions(options)) return void 0;
19615
19674
  if (isGooglePayload(payload)) {
19616
- const config = isRecord(payload.config) ? payload.config : {};
19675
+ const config = isRecord$1(payload.config) ? payload.config : {};
19617
19676
  return {
19618
19677
  ...payload,
19619
19678
  config: applyConfigOptions(config, options)
19620
19679
  };
19621
19680
  }
19622
19681
  if (isBedrockPayload(payload)) {
19623
- const inferenceConfig = isRecord(payload.inferenceConfig) ? payload.inferenceConfig : {};
19682
+ const inferenceConfig = isRecord$1(payload.inferenceConfig) ? payload.inferenceConfig : {};
19624
19683
  return {
19625
19684
  ...payload,
19626
19685
  inferenceConfig: applyBedrockOptions(inferenceConfig, options)
@@ -19673,11 +19732,11 @@ function isAnthropicPayload(payload) {
19673
19732
  return "anthropic_version" in payload;
19674
19733
  }
19675
19734
  function hasActiveThinking(value) {
19676
- if (!isRecord(value)) return false;
19735
+ if (!isRecord$1(value)) return false;
19677
19736
  const type = value.type;
19678
19737
  return type !== "disabled" && type !== "off" && type !== false;
19679
19738
  }
19680
- function isRecord(value) {
19739
+ function isRecord$1(value) {
19681
19740
  return typeof value === "object" && value !== null && !Array.isArray(value);
19682
19741
  }
19683
19742
  //#endregion
@@ -19860,7 +19919,7 @@ function getSubmitOutputContract(taskType) {
19860
19919
  return {
19861
19920
  toolName: submitOutputToolName(taskType),
19862
19921
  taskType,
19863
- description: `Submit the structured output for this ${taskType} task. Call exactly once when done. The arguments below ARE the output payload — pass each top-level field of the task type's output schema directly. The runtime validates the args against the schema; mismatches return a tool error you can recover from in the same session. On a valid call the runtime captures the payload and ends the session — you do not need to repeat the JSON in your final assistant message.`,
19922
+ description: `Submit the structured output for this ${taskType} task. Call exactly once when done. The arguments below ARE the output payload — pass each top-level field of the task type's output schema directly. The runtime validates the args against the schema; mismatches return a tool error you can recover from in the same session. On a valid call the runtime captures the payload for attempt completion — you do not need to repeat the JSON in your final assistant message.`,
19864
19923
  parametersSchema: schema
19865
19924
  };
19866
19925
  }
@@ -19911,7 +19970,7 @@ function buildFinalOutputBlock(opts) {
19911
19970
  `output matching \`${outputSchemaName}\`.`,
19912
19971
  "",
19913
19972
  `Call \`${submitTool}\` exactly once with the payload.`,
19914
- `The runtime captures the validated arguments and ends the session.`,
19973
+ `The runtime captures the validated arguments for attempt completion.`,
19915
19974
  `Do NOT emit the output as plain assistant text. Do NOT rely on a`,
19916
19975
  `JSON-in-message fallback. If you do not call \`${submitTool}\`, the`,
19917
19976
  `attempt is recorded as failing the promised submit-output criterion`,
@@ -20650,7 +20709,7 @@ function buildJudgeEvalAttemptUserPrompt(input, ctx) {
20650
20709
  ` "targetTaskId": "${input.targetTaskId}",`,
20651
20710
  ` "targetAttemptN": ${input.targetAttemptN},`,
20652
20711
  " \"variantLabel\": \"<from producer input>\",",
20653
- " \"scores\": [ { \"criterionId\": \"...\", \"score\": 0..1, \"rationale\": \"...\", \"assertions\": [...]? } ],",
20712
+ " \"scores\": [ { \"criterionId\": \"...\", \"score\": 0..1, \"rationale\": \"...\", \"assertions\": [...]?, \"evidence\": { \"text\": \"...\" } } ],",
20654
20713
  " \"composite\": <Σ(weight × score), 0..1>,",
20655
20714
  " \"verdict\": \"<1-3 sentences>\",",
20656
20715
  " \"judgeModel\": \"<id>\", // optional",
@@ -24897,8 +24956,172 @@ async function resolvePriorContext(agent, continueFrom) {
24897
24956
  };
24898
24957
  }
24899
24958
  //#endregion
24959
+ //#region src/runtime/retry-triage.ts
24960
+ var MAX_TRIAGE_JSON_CHARS = 12e3;
24961
+ var MAX_TRIAGE_FIELD_CHARS = 2e3;
24962
+ var REDACTED = "[redacted]";
24963
+ var SECRET_KEY_PATTERN = /(?:api[_-]?key|token|secret|password|passwd|credential|authorization|private[_-]?key|access[_-]?token|refresh[_-]?token)/i;
24964
+ function createPiRetryTriage(options) {
24965
+ return async (input) => {
24966
+ const cwd = options.cwd ?? process.cwd();
24967
+ const capture = createRetryTriageTool();
24968
+ const resourceLoader = new DefaultResourceLoader({
24969
+ cwd,
24970
+ agentDir: options.piAgentDir,
24971
+ appendSystemPrompt: [TRIAGE_SYSTEM_PROMPT],
24972
+ skillsOverride: () => ({
24973
+ skills: [],
24974
+ diagnostics: []
24975
+ })
24976
+ });
24977
+ await resourceLoader.reload();
24978
+ const sessionManager = SessionManager.inMemory(cwd);
24979
+ const created = await createAgentSession({
24980
+ agentDir: options.piAgentDir,
24981
+ cwd,
24982
+ model: options.model,
24983
+ thinkingLevel: options.thinkingLevel ?? void 0,
24984
+ customTools: [capture.tool],
24985
+ sessionManager,
24986
+ resourceLoader
24987
+ });
24988
+ await withTimeout(created.session.prompt(buildTriagePrompt(input)), options.timeoutMs ?? 3e4, () => created.session.abort());
24989
+ const result = capture.getCaptured();
24990
+ if (!result) throw new Error("Retry triage did not submit a decision");
24991
+ return normalizeRetryTriageResult(result);
24992
+ };
24993
+ }
24994
+ function createRetryTriageTool() {
24995
+ let captured = null;
24996
+ return {
24997
+ tool: defineTool({
24998
+ name: "submit_retry_triage",
24999
+ label: "Submit retry triage",
25000
+ description: "Submit the retry decision for a failed MoltNet task attempt.",
25001
+ parameters: {
25002
+ type: "object",
25003
+ additionalProperties: false,
25004
+ required: [
25005
+ "decision",
25006
+ "confidence",
25007
+ "reason"
25008
+ ],
25009
+ properties: {
25010
+ decision: {
25011
+ type: "string",
25012
+ enum: ["retry", "do_not_retry"]
25013
+ },
25014
+ confidence: {
25015
+ type: "string",
25016
+ enum: [
25017
+ "low",
25018
+ "medium",
25019
+ "high"
25020
+ ]
25021
+ },
25022
+ reason: {
25023
+ type: "string",
25024
+ minLength: 1
25025
+ }
25026
+ }
25027
+ },
25028
+ execute(_id, params) {
25029
+ captured = normalizeRetryTriageResult(params);
25030
+ return Promise.resolve({
25031
+ content: [{
25032
+ type: "text",
25033
+ text: "Retry triage captured."
25034
+ }],
25035
+ details: captured,
25036
+ terminate: true
25037
+ });
25038
+ }
25039
+ }),
25040
+ getCaptured: () => captured
25041
+ };
25042
+ }
25043
+ function normalizeRetryTriageResult(value) {
25044
+ const record = value && typeof value === "object" ? value : {};
25045
+ return {
25046
+ decision: record.decision === "retry" ? "retry" : "do_not_retry",
25047
+ confidence: record.confidence === "high" || record.confidence === "medium" ? record.confidence : "low",
25048
+ reason: typeof record.reason === "string" && record.reason.trim() ? record.reason.trim().slice(0, 500) : "retry triage did not provide a reason"
25049
+ };
25050
+ }
25051
+ function buildTriagePrompt(input) {
25052
+ const payload = {
25053
+ task: {
25054
+ id: input.task.id,
25055
+ type: input.task.taskType,
25056
+ teamId: input.task.teamId,
25057
+ input: prepareTriagePayload(input.task.input)
25058
+ },
25059
+ attempt: {
25060
+ attemptN: input.attemptN,
25061
+ maxAttempts: input.maxAttempts ?? null,
25062
+ remainingAttempts: input.remainingAttempts ?? null
25063
+ },
25064
+ error: prepareTriagePayload(input.error),
25065
+ recentMessages: prepareTriagePayload((input.recentMessages ?? []).slice(-12))
25066
+ };
25067
+ return [
25068
+ "Classify whether this failed task attempt should be retried.",
25069
+ "",
25070
+ "Retry only when a fresh attempt can plausibly recover without changing the task input.",
25071
+ "Do not retry for policy, validation, credentials, cancellation, model/config, or task-contract failures.",
25072
+ "Submit-output validation errors should have been corrected inside the active Pi session; if one reaches retry triage, treat it as exhausted and choose do_not_retry.",
25073
+ "Use confidence=low when evidence is weak; low confidence must choose do_not_retry.",
25074
+ "Call submit_retry_triage exactly once.",
25075
+ "",
25076
+ truncateString(JSON.stringify(payload, null, 2), MAX_TRIAGE_JSON_CHARS)
25077
+ ].join("\n");
25078
+ }
25079
+ function prepareTriagePayload(value) {
25080
+ return redactAndTruncate(value, []);
25081
+ }
25082
+ function redactAndTruncate(value, path) {
25083
+ const currentKey = path[path.length - 1] ?? "";
25084
+ if (SECRET_KEY_PATTERN.test(currentKey)) return REDACTED;
25085
+ if (typeof value === "string") return truncateString(redactRetryTriageSecrets(value), MAX_TRIAGE_FIELD_CHARS);
25086
+ if (Array.isArray(value)) return value.map((item, index) => redactAndTruncate(item, [...path, String(index)]));
25087
+ if (value && typeof value === "object") {
25088
+ const entries = Object.entries(value).map(([key, child]) => [key, redactAndTruncate(child, [...path, key])]);
25089
+ return Object.fromEntries(entries);
25090
+ }
25091
+ return value;
25092
+ }
25093
+ function redactRetryTriageSecrets(value) {
25094
+ 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);
25095
+ }
25096
+ function truncateString(value, maxChars) {
25097
+ if (value.length <= maxChars) return value;
25098
+ return `${value.slice(0, maxChars)}...[truncated ${value.length - maxChars} chars]`;
25099
+ }
25100
+ var TRIAGE_SYSTEM_PROMPT = [
25101
+ "You are MoltNet retry triage.",
25102
+ "You classify one failed execution attempt, not the whole task.",
25103
+ "Return retry only for likely transient/runtime failures or clear evidence a new attempt can recover.",
25104
+ "The agent may have already tried local recovery; do not ask for more work."
25105
+ ].join("\n");
25106
+ async function withTimeout(promise, timeoutMs, onTimeout) {
25107
+ let timeout;
25108
+ const timeoutPromise = new Promise((_, reject) => {
25109
+ timeout = setTimeout(() => {
25110
+ Promise.resolve(onTimeout?.()).catch(() => {});
25111
+ reject(/* @__PURE__ */ new Error(`Retry triage timed out after ${timeoutMs}ms`));
25112
+ }, timeoutMs);
25113
+ });
25114
+ try {
25115
+ return await Promise.race([promise, timeoutPromise]);
25116
+ } finally {
25117
+ if (timeout) clearTimeout(timeout);
25118
+ }
25119
+ }
25120
+ //#endregion
24900
25121
  //#region src/runtime/subagent-tool.ts
24901
25122
  var SUBAGENT_SUBMIT_TOOL_NAME = "submit_subagent_output";
25123
+ var DEFAULT_SUBAGENT_SUBMIT_VALIDATION_RETRIES = 2;
25124
+ var RecoverableSubagentSubmitParameters = _Object_({}, { additionalProperties: Unknown() });
24902
25125
  /**
24903
25126
  * Parameters shape the parent LLM sees when calling the subagent tool.
24904
25127
  *
@@ -24942,21 +25165,44 @@ function createSubagentTool(args) {
24942
25165
  callCount += 1;
24943
25166
  const callIndex = callCount;
24944
25167
  let captured = null;
25168
+ let innerInvalidSubmitCount = 0;
25169
+ let innerValidationFailure = null;
25170
+ let innerValidationExhausted = false;
24945
25171
  const submitTool = defineTool({
24946
25172
  name: SUBAGENT_SUBMIT_TOOL_NAME,
24947
25173
  label: `Submit ${output_schema}`,
24948
25174
  description: `Submit your structured output for this subagent task. Call exactly once when done. Args MUST match the ${output_schema} contract; mismatches return a tool error you can recover from in the same session.`,
24949
- parameters: contract.parametersSchema,
25175
+ promptSnippet: `${SUBAGENT_SUBMIT_TOOL_NAME}: submit the final structured ${output_schema} payload.`,
25176
+ promptGuidelines: [`Call \`${SUBAGENT_SUBMIT_TOOL_NAME}\` with the exact \`${output_schema}\` contract shape.`, "If the submit tool returns a validation error, fix every listed field and call the same tool again."],
25177
+ parameters: RecoverableSubagentSubmitParameters,
24950
25178
  async execute(_innerId, innerParams) {
24951
- if (!Check(contract.parametersSchema, innerParams)) return toolError(`submit_subagent_output: schema validation failed: ${[...Errors(contract.parametersSchema, innerParams)].slice(0, 3).map((e) => `${e.instancePath}: ${e.message}`).join("; ")}. Re-call with a corrected payload.`);
25179
+ if (innerValidationExhausted) return toolError("submit_subagent_output validation retry budget is already exhausted.", {
25180
+ captured: false,
25181
+ error: "output_validation_failed",
25182
+ invalidCallCount: innerInvalidSubmitCount,
25183
+ maxSubmitValidationRetries: DEFAULT_SUBAGENT_SUBMIT_VALIDATION_RETRIES
25184
+ });
25185
+ if (!Check(contract.parametersSchema, innerParams)) {
25186
+ innerInvalidSubmitCount += 1;
25187
+ const maxInvalidCalls = DEFAULT_SUBAGENT_SUBMIT_VALIDATION_RETRIES + 1;
25188
+ const exhausted = innerInvalidSubmitCount >= maxInvalidCalls;
25189
+ const errs = [...Errors(contract.parametersSchema, innerParams)].map((e) => `${e.instancePath}: ${e.message}`).join("; ");
25190
+ innerValidationFailure = `submit_subagent_output validation failed (${innerInvalidSubmitCount}/${maxInvalidCalls}): ${errs}. ` + (exhausted ? "Validation retry budget exhausted." : "Re-call with a corrected payload.");
25191
+ if (exhausted) innerValidationExhausted = true;
25192
+ return toolError(innerValidationFailure, {
25193
+ captured: false,
25194
+ error: "output_validation_failed",
25195
+ invalidCallCount: innerInvalidSubmitCount,
25196
+ maxSubmitValidationRetries: DEFAULT_SUBAGENT_SUBMIT_VALIDATION_RETRIES
25197
+ });
25198
+ }
24952
25199
  captured = innerParams;
24953
25200
  return {
24954
25201
  content: [{
24955
25202
  type: "text",
24956
- text: "Output captured. Subagent session will terminate; no further action needed."
25203
+ text: "Output captured. No further action needed for subagent output reporting."
24957
25204
  }],
24958
- details: { captured: true },
24959
- terminate: true
25205
+ details: { captured: true }
24960
25206
  };
24961
25207
  }
24962
25208
  });
@@ -25020,7 +25266,16 @@ function createSubagentTool(args) {
25020
25266
  if (cancelListener) cancelListener();
25021
25267
  }
25022
25268
  if (abortReason !== null) return toolError(`subagent: ${abortReason === "subagent_timed_out" ? `subagent timed out after ${timeoutMs}ms` : "parent task was cancelled"}. The parent should fail this task or retry with a clearer scope.`);
25023
- if (captured === null) return toolError(`subagent: inner session ended without calling ${SUBAGENT_SUBMIT_TOOL_NAME}. The parent should retry with clearer instructions or fail the task.`);
25269
+ if (captured === null) {
25270
+ const exhaustedFailure = innerValidationFailure;
25271
+ if (innerValidationExhausted && exhaustedFailure) return toolError(`subagent: ${exhaustedFailure}`, {
25272
+ captured: false,
25273
+ error: "output_validation_failed",
25274
+ invalidCallCount: innerInvalidSubmitCount,
25275
+ maxSubmitValidationRetries: DEFAULT_SUBAGENT_SUBMIT_VALIDATION_RETRIES
25276
+ });
25277
+ return toolError(`subagent: inner session ended without calling ${SUBAGENT_SUBMIT_TOOL_NAME}. The parent should retry with clearer instructions or fail the task.`);
25278
+ }
25024
25279
  return {
25025
25280
  content: [{
25026
25281
  type: "text",
@@ -25065,8 +25320,7 @@ function buildSubagentInstructor(args) {
25065
25320
  "Rules for this session:",
25066
25321
  "",
25067
25322
  `- You MUST call \`${SUBAGENT_SUBMIT_TOOL_NAME}\` exactly once with a `,
25068
- " payload matching the contract above. Your session terminates on ",
25069
- " the valid call.",
25323
+ " payload matching the contract above as your final task action.",
25070
25324
  "- The parent's message above is your task. Do not invent additional ",
25071
25325
  " steps the parent did not request.",
25072
25326
  "- All MoltNet runtime invariants from the parent runtime instructor ",
@@ -25077,13 +25331,13 @@ function buildSubagentInstructor(args) {
25077
25331
  " delegation; do the work yourself."
25078
25332
  ].join("\n");
25079
25333
  }
25080
- function toolError(text) {
25334
+ function toolError(text, details = { captured: false }) {
25081
25335
  return {
25082
25336
  content: [{
25083
25337
  type: "text",
25084
25338
  text
25085
25339
  }],
25086
- details: { captured: false },
25340
+ details,
25087
25341
  isError: true
25088
25342
  };
25089
25343
  }
@@ -25230,25 +25484,112 @@ var UnknownTaskTypeForSubmitToolError = class extends Error {
25230
25484
  this.name = "UnknownTaskTypeForSubmitToolError";
25231
25485
  }
25232
25486
  };
25487
+ var DEFAULT_MAX_SUBMIT_VALIDATION_RETRIES = 2;
25488
+ var RecoverableSubmitToolParameters = _Object_({}, { additionalProperties: Unknown() });
25489
+ function formatValidationErrors(errors) {
25490
+ return errors.map((err) => `${err.field}: ${err.message}`).join("; ");
25491
+ }
25492
+ function submitOutputRepairHint(taskType, errors) {
25493
+ const fields = new Set(errors.map((err) => err.field));
25494
+ const hints = ["Tool args must be the output object directly, not wrapped in { output: ... }."];
25495
+ if (fields.has("output/artifacts")) hints.push("`artifacts` must be an array; omit it when there are no artifacts, use [], or use objects like { \"kind\": \"note\", \"title\": \"Result\", \"body\": \"...\" }.");
25496
+ if (fields.has("output/verification")) hints.push("`verification` must be an object with inputCid, results[], and passed; do not send it as text or an array.");
25497
+ if (taskType === "freeform" && (fields.has("output/artifacts") || fields.has("output/verification"))) hints.push("Minimal valid freeform retry: { \"summary\": \"completed\", \"artifacts\": [], \"verification\": { \"inputCid\": \"<task inputCid>\", \"results\": [{ \"id\": \"submit-output\", \"kind\": \"gate\", \"status\": \"pass\", \"detail\": \"submit_freeform_output accepted valid args\" }], \"passed\": true } }.");
25498
+ if (hints.length === 1) hints.push("Fix every listed field before re-calling this same tool.");
25499
+ return hints.join(" ");
25500
+ }
25501
+ function isRecord(value) {
25502
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25503
+ }
25504
+ function onlySubmitOutputGate(input) {
25505
+ if (!isRecord(input) || !isRecord(input.successCriteria)) return false;
25506
+ const criteria = input.successCriteria;
25507
+ const gates = criteria.gates;
25508
+ if (!Array.isArray(gates) || gates.length !== 1) return false;
25509
+ const [gate] = gates;
25510
+ if (!isRecord(gate) || gate.id !== "submit-output") return false;
25511
+ const assertions = criteria.assertions;
25512
+ if (Array.isArray(assertions) && assertions.length > 0) return false;
25513
+ return criteria.rubric === void 0 && criteria.sideEffects === void 0 && criteria.minComposite === void 0;
25514
+ }
25515
+ function repairFreeformSubmitOutput(params, opts) {
25516
+ if (!isRecord(params) || !opts.inputCid || !onlySubmitOutputGate(opts.input)) return null;
25517
+ const repaired = { ...params };
25518
+ if ("artifacts" in repaired && !Array.isArray(repaired.artifacts)) if (isRecord(repaired.artifacts)) repaired.artifacts = [repaired.artifacts];
25519
+ else delete repaired.artifacts;
25520
+ if ("proposedTaskType" in repaired && !isRecord(repaired.proposedTaskType)) if (typeof repaired.proposedTaskType === "string" && repaired.proposedTaskType.length > 0) repaired.proposedTaskType = {
25521
+ name: repaired.proposedTaskType,
25522
+ rationale: "Suggested by the model during freeform execution."
25523
+ };
25524
+ else delete repaired.proposedTaskType;
25525
+ repaired.verification = {
25526
+ inputCid: opts.inputCid,
25527
+ results: [{
25528
+ id: SUBMIT_OUTPUT_GATE_ID,
25529
+ kind: "gate",
25530
+ status: "pass",
25531
+ detail: "submit_freeform_output accepted valid args"
25532
+ }],
25533
+ passed: true
25534
+ };
25535
+ return repaired;
25536
+ }
25537
+ function maybeRepairSubmitOutput(taskType, params, opts) {
25538
+ if (taskType !== "freeform") return null;
25539
+ const repaired = repairFreeformSubmitOutput(params, opts);
25540
+ if (!repaired) return null;
25541
+ return validateTaskOutput(taskType, repaired, opts.input).length === 0 ? repaired : null;
25542
+ }
25233
25543
  function createSubmitOutputTool(taskType, opts = {}) {
25234
25544
  const contract = getSubmitOutputContract(taskType);
25235
25545
  if (!contract) throw new UnknownTaskTypeForSubmitToolError(taskType);
25236
- const schema = contract.parametersSchema;
25546
+ const maxSubmitValidationRetries = opts.maxSubmitValidationRetries ?? DEFAULT_MAX_SUBMIT_VALIDATION_RETRIES;
25237
25547
  let captured = null;
25238
25548
  let callCount = 0;
25549
+ let invalidCallCount = 0;
25550
+ let lastValidationFailure = null;
25551
+ let exhaustedValidationFailure = null;
25239
25552
  return {
25240
25553
  tool: defineTool({
25241
25554
  name: contract.toolName,
25242
25555
  label: `Submit ${taskType} output`,
25243
25556
  description: contract.description,
25244
- parameters: schema,
25557
+ promptSnippet: `${contract.toolName}: submit the final structured ${taskType} output using the schema shown in the task prompt.`,
25558
+ promptGuidelines: [`Call \`${contract.toolName}\` with the exact ${taskType} output shape shown in the task prompt.`, "If the submit tool returns a validation error, fix every listed field and call the same tool again."],
25559
+ parameters: RecoverableSubmitToolParameters,
25245
25560
  async execute(_id, params) {
25246
- const errors = validateTaskOutput(taskType, params, opts.input);
25561
+ if (exhaustedValidationFailure) return {
25562
+ content: [{
25563
+ type: "text",
25564
+ text: "Submit-output validation retry budget is already exhausted; the attempt will fail."
25565
+ }],
25566
+ details: {
25567
+ captured: false,
25568
+ callCount,
25569
+ invalidCallCount,
25570
+ maxSubmitValidationRetries,
25571
+ error: "output_validation_failed"
25572
+ },
25573
+ isError: true
25574
+ };
25575
+ const candidateParams = maybeRepairSubmitOutput(taskType, params, opts) ?? params;
25576
+ const errors = validateTaskOutput(taskType, candidateParams, opts.input);
25247
25577
  if (errors.length > 0) {
25248
- const detailMsg = errors.slice(0, 3).map((err) => `${err.field}: ${err.message}`).join("; ");
25578
+ invalidCallCount += 1;
25579
+ const detailMsg = formatValidationErrors(errors);
25580
+ const maxInvalidCalls = maxSubmitValidationRetries + 1;
25581
+ const exhausted = invalidCallCount >= maxInvalidCalls;
25582
+ const message = `Output failed validation (${invalidCallCount}/${maxInvalidCalls}): ${detailMsg}. ${submitOutputRepairHint(taskType, errors)} ` + (exhausted ? "Submit-output validation retry budget exhausted; the attempt will fail." : "Re-call this tool with a corrected output.");
25583
+ lastValidationFailure = {
25584
+ code: "output_validation_failed",
25585
+ message
25586
+ };
25587
+ if (exhausted) exhaustedValidationFailure = lastValidationFailure;
25249
25588
  const details = {
25250
25589
  captured: false,
25251
25590
  callCount,
25591
+ invalidCallCount,
25592
+ maxSubmitValidationRetries,
25252
25593
  error: "output_validation_failed"
25253
25594
  };
25254
25595
  recordTaskOutputParseResult({
@@ -25259,13 +25600,13 @@ function createSubmitOutputTool(taskType, opts = {}) {
25259
25600
  return {
25260
25601
  content: [{
25261
25602
  type: "text",
25262
- text: `Output failed validation: ${detailMsg}. Re-call this tool with a corrected output.`
25603
+ text: message
25263
25604
  }],
25264
25605
  details,
25265
25606
  isError: true
25266
25607
  };
25267
25608
  }
25268
- captured = params;
25609
+ captured = candidateParams;
25269
25610
  callCount += 1;
25270
25611
  return {
25271
25612
  content: [{
@@ -25276,13 +25617,15 @@ function createSubmitOutputTool(taskType, opts = {}) {
25276
25617
  captured: true,
25277
25618
  callCount,
25278
25619
  error: null
25279
- },
25280
- terminate: true
25620
+ }
25281
25621
  };
25282
25622
  }
25283
25623
  }),
25284
25624
  getCaptured: () => captured,
25285
- getCallCount: () => callCount
25625
+ getCallCount: () => callCount,
25626
+ getInvalidCallCount: () => invalidCallCount,
25627
+ getLastValidationFailure: () => lastValidationFailure,
25628
+ getExhaustedValidationFailure: () => exhaustedValidationFailure
25286
25629
  };
25287
25630
  }
25288
25631
  /**
@@ -25851,7 +26194,9 @@ async function executePiTask(claimedTask, reporter, opts) {
25851
26194
  });
25852
26195
  const { handle: submitToolHandle, tools: submitToolDefs } = resolveSubmitTools(task.taskType, {
25853
26196
  model: opts.model,
25854
- input: task.input
26197
+ input: task.input,
26198
+ inputCid: task.inputCid,
26199
+ maxSubmitValidationRetries: opts.maxSubmitValidationRetries
25855
26200
  });
25856
26201
  const submitTools = submitToolDefs;
25857
26202
  try {
@@ -26038,19 +26383,28 @@ async function executePiTask(claimedTask, reporter, opts) {
26038
26383
  }
26039
26384
  });
26040
26385
  let runError = null;
26041
- try {
26042
- await session.prompt(taskPrompt);
26043
- } catch (err) {
26044
- const message = err instanceof Error ? err.message : String(err);
26045
- runError = {
26046
- code: "session_prompt_failed",
26047
- message
26048
- };
26049
- await emit("error", {
26386
+ runError = (await promptWithProviderErrorRetries({
26387
+ session,
26388
+ initialPrompt: taskPrompt,
26389
+ cancelSignal: reporter.cancelSignal,
26390
+ isCapAborted: () => capAbort !== null,
26391
+ getProviderErrorState: () => ({
26392
+ llmAbort,
26393
+ llmErrorMessage
26394
+ }),
26395
+ maxRetries: opts.maxProviderErrorRetries ?? 2,
26396
+ baseDelayMs: opts.providerErrorRetryBaseDelayMs ?? 2e3,
26397
+ maxDelayMs: opts.providerErrorRetryMaxDelayMs ?? 3e4,
26398
+ retryPrompt: opts.providerErrorRetryPrompt ?? "Go on",
26399
+ onRetry: async (event) => {
26400
+ await emit("info", event);
26401
+ await notifyProviderErrorRetryUi(opts.providerErrorRetryUi, event);
26402
+ },
26403
+ onPromptError: (message) => emit("error", {
26050
26404
  message,
26051
26405
  phase: "session_prompt"
26052
- });
26053
- }
26406
+ })
26407
+ })).runError;
26054
26408
  if (subagentHandle && subagentHandle.getCallCount() > 0) await emit("info", {
26055
26409
  event: "subagent_summary",
26056
26410
  callCount: subagentHandle.getCallCount()
@@ -26089,7 +26443,7 @@ async function executePiTask(claimedTask, reporter, opts) {
26089
26443
  });
26090
26444
  }
26091
26445
  else if (submitToolHandle) {
26092
- parseError = {
26446
+ parseError = submitToolHandle.getExhaustedValidationFailure() ?? {
26093
26447
  code: "submit_output_missing",
26094
26448
  message: "Agent did not satisfy the promised submit-output criterion: no valid task submit tool call was captured before the session ended."
26095
26449
  };
@@ -26281,6 +26635,110 @@ function shouldEmitToolCallError(event) {
26281
26635
  if (event.toolName === "bash") return false;
26282
26636
  return true;
26283
26637
  }
26638
+ var PROVIDER_ERROR_NON_RETRYABLE_PATTERNS = [
26639
+ /\b401\b/i,
26640
+ /\b403\b/i,
26641
+ /\bunauthori[sz]ed\b/i,
26642
+ /\bforbidden\b/i,
26643
+ /\binvalid (?:api )?key\b/i,
26644
+ /\bmissing credentials?\b/i,
26645
+ /\binsufficient[_\s-]?quota\b/i,
26646
+ /\bbilling\b/i,
26647
+ /\bmodel .*not (?:found|registered|available)\b/i,
26648
+ /\bunknown model\b/i
26649
+ ];
26650
+ var PROVIDER_ERROR_RETRYABLE_PATTERNS = [
26651
+ /\b429\b/i,
26652
+ /\b5(?:02|03|04)\b/i,
26653
+ /\btimeout\b/i,
26654
+ /\btimed out\b/i,
26655
+ /\brate limit/i,
26656
+ /\btemporar(?:y|ily)\b/i,
26657
+ /\bunavailable\b/i,
26658
+ /\boverloaded\b/i,
26659
+ /\bECONNRESET\b/i,
26660
+ /\bECONNREFUSED\b/i,
26661
+ /\bETIMEDOUT\b/i,
26662
+ /\bENOTFOUND\b/i,
26663
+ /\bEAI_AGAIN\b/i,
26664
+ /\bDNS\b/i
26665
+ ];
26666
+ function shouldRetryProviderErrorMessage(message) {
26667
+ if (!message || !message.trim()) return true;
26668
+ if (PROVIDER_ERROR_NON_RETRYABLE_PATTERNS.some((pattern) => pattern.test(message))) return false;
26669
+ if (PROVIDER_ERROR_RETRYABLE_PATTERNS.some((pattern) => pattern.test(message))) return true;
26670
+ return true;
26671
+ }
26672
+ function computeProviderErrorRetryDelay(attempt, baseDelayMs, maxDelayMs) {
26673
+ return Math.min(Math.max(0, baseDelayMs) * 2 ** (attempt - 1), Math.max(0, maxDelayMs));
26674
+ }
26675
+ function formatProviderErrorRetryStatus(event) {
26676
+ const seconds = Math.ceil(event.delayMs / 1e3);
26677
+ return `Provider retry ${event.retry}/${event.maxRetries} in ${seconds}s`;
26678
+ }
26679
+ function formatProviderErrorRetryNotification(event) {
26680
+ return `Provider error; retrying same Pi session (${event.retry}/${event.maxRetries}).`;
26681
+ }
26682
+ async function notifyProviderErrorRetryUi(ui, event) {
26683
+ if (!ui || ui.hasUI === false) return;
26684
+ await ui.setStatus?.("provider_retry", formatProviderErrorRetryStatus(event));
26685
+ await ui.notify?.(formatProviderErrorRetryNotification(event), "warning");
26686
+ }
26687
+ async function promptWithProviderErrorRetries(args) {
26688
+ let retryCount = 0;
26689
+ let promptText = args.initialPrompt;
26690
+ while (true) {
26691
+ try {
26692
+ await args.session.prompt(promptText);
26693
+ } catch (err) {
26694
+ const message = err instanceof Error ? err.message : String(err);
26695
+ await args.onPromptError?.(message);
26696
+ return {
26697
+ runError: {
26698
+ code: "session_prompt_failed",
26699
+ message
26700
+ },
26701
+ retryCount
26702
+ };
26703
+ }
26704
+ const { llmAbort, llmErrorMessage } = args.getProviderErrorState();
26705
+ if (!llmAbort || args.cancelSignal.aborted || args.isCapAborted?.() || retryCount >= args.maxRetries || !shouldRetryProviderErrorMessage(llmErrorMessage)) return {
26706
+ runError: null,
26707
+ retryCount
26708
+ };
26709
+ retryCount += 1;
26710
+ const delayMs = computeProviderErrorRetryDelay(retryCount, args.baseDelayMs, args.maxDelayMs);
26711
+ await args.onRetry?.({
26712
+ event: "provider_error_retry",
26713
+ retry: retryCount,
26714
+ maxRetries: args.maxRetries,
26715
+ delayMs,
26716
+ reason: sanitizeProviderErrorRetryReason(llmErrorMessage)
26717
+ });
26718
+ await sleepUnlessAborted(delayMs, args.cancelSignal);
26719
+ if (args.cancelSignal.aborted || args.isCapAborted?.()) return {
26720
+ runError: null,
26721
+ retryCount
26722
+ };
26723
+ promptText = args.retryPrompt;
26724
+ }
26725
+ }
26726
+ function sanitizeProviderErrorRetryReason(value) {
26727
+ return redactRetryTriageSecrets(value ?? "Pi turn ended with stopReason=error").slice(0, 500);
26728
+ }
26729
+ async function sleepUnlessAborted(delayMs, signal) {
26730
+ if (delayMs <= 0 || signal.aborted) return;
26731
+ await new Promise((resolve) => {
26732
+ const timeout = setTimeout(done, delayMs);
26733
+ const onAbort = () => done();
26734
+ function done() {
26735
+ clearTimeout(timeout);
26736
+ signal.removeEventListener("abort", onAbort);
26737
+ resolve();
26738
+ }
26739
+ signal.addEventListener("abort", onAbort, { once: true });
26740
+ });
26741
+ }
26284
26742
  /**
26285
26743
  * Detect pi's bash-timeout error wrapper in a `tool_execution_end`
26286
26744
  * result. The bash tool surfaces a timeout as a structured tool result
@@ -26352,6 +26810,19 @@ function describeToolErrorMessage(result) {
26352
26810
  * See README.md for credential injection flow, tool split, sandbox.json
26353
26811
  * reference, and headless/programmatic usage.
26354
26812
  */
26813
+ function createPiProviderErrorRetryUi(ctx) {
26814
+ const hasUI = Boolean(ctx.hasUI);
26815
+ if (!hasUI) return void 0;
26816
+ return {
26817
+ hasUI,
26818
+ setStatus: (key, message) => {
26819
+ ctx.ui.setStatus(key, ctx.ui.theme.fg("muted", message));
26820
+ },
26821
+ notify: (message, level) => {
26822
+ ctx.ui.notify?.(message, level);
26823
+ }
26824
+ };
26825
+ }
26355
26826
  function moltnetExtension(pi) {
26356
26827
  pi.registerFlag("agent", {
26357
26828
  description: "MoltNet agent name (required — pass --agent <name>)",
@@ -26642,4 +27113,4 @@ function moltnetExtension(pi) {
26642
27113
  registerMoltnetReflectCommand(pi, state);
26643
27114
  }
26644
27115
  //#endregion
26645
- 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 };
27116
+ export { HOST_EXEC_DEFAULT_BASE_ENV, activateAgentEnv, buildAgentSession, createGondolinBashOps, createGondolinEditOps, createGondolinReadOps, createGondolinWriteOps, createMoltNetTools, createPiOtelExtension, createPiProviderErrorRetryUi, 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.30.0",
3
+ "version": "0.31.1",
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.33.0",
40
- "@themoltnet/sdk": "0.116.0"
39
+ "@themoltnet/agent-runtime": "0.33.2",
40
+ "@themoltnet/sdk": "0.117.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@earendil-works/pi-coding-agent": ">=0.74.0",