pi-cohort 7.4.0 → 7.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [7.4.1] - 2026-10-19
4
+
5
+ ### Fixed
6
+ - Non-BLOCKED file-only failures and save errors return an artifact pointer or an explicit not-preserved reason instead of the child reply inline; BLOCKED handoffs and inline mode are unchanged. (#24)
7
+
3
8
  ## [7.4.0] - 2026-10-19
4
9
 
5
10
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cohort",
3
- "version": "7.4.0",
3
+ "version": "7.4.1",
4
4
  "description": "Delegate Pi work to focused child agents: code review, scouting, implementation, parallel audits, saved chains, and background jobs.",
5
5
  "author": "Jacek Juraszek",
6
6
  "license": "MIT",
@@ -7,7 +7,7 @@ import type { Message } from "@earendil-works/pi-ai";
7
7
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
8
8
  import { appendJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
9
9
  import { getPiSpawnCommand, parsePiRuntime, PiRuntimeLaunchError, type PiRuntimeIdentity } from "../shared/pi-spawn.ts";
10
- import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
10
+ import { captureSingleOutputSnapshot, finalizeSingleOutput, formatFileOnlyFailure, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
11
11
  import {
12
12
  type ActivityState,
13
13
  type ArtifactConfig,
@@ -1007,6 +1007,8 @@ export async function runSingleStep(
1007
1007
  let acceptanceCanFailRun = false;
1008
1008
  let effectiveFinalExitCode = resolvedOutput.saveError ? 1 : finalResult?.exitCode ?? 1;
1009
1009
  let effectiveFinalError = finalResult?.error;
1010
+ let preservedAt: string | undefined;
1011
+ let notPreservedReason = "artifact output disabled";
1010
1012
  if (resolvedOutput.saveError) {
1011
1013
  effectiveFinalError = effectiveFinalError
1012
1014
  ? `${effectiveFinalError}\n${resolvedOutput.saveError}`
@@ -1037,7 +1039,14 @@ export async function runSingleStep(
1037
1039
 
1038
1040
  if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
1039
1041
  if (ctx.artifactConfig?.includeOutput !== false) {
1040
- fs.writeFileSync(artifactPaths.outputPath, output, "utf-8");
1042
+ try {
1043
+ fs.writeFileSync(artifactPaths.outputPath, output, "utf-8");
1044
+ preservedAt = artifactPaths.outputPath;
1045
+ } catch (error) {
1046
+ const message = error instanceof Error ? error.message : String(error);
1047
+ notPreservedReason = `artifact write failed: ${message}`;
1048
+ throw error;
1049
+ }
1041
1050
  }
1042
1051
  if (ctx.artifactConfig?.includeMetadata !== false) {
1043
1052
  fs.writeFileSync(
@@ -1063,11 +1072,14 @@ export async function runSingleStep(
1063
1072
  effectiveFinalError = effectiveFinalError ? `${effectiveFinalError}\n${message}` : message;
1064
1073
  }
1065
1074
 
1075
+ const outputBeforeExternalFinalization = outputForSummary;
1076
+ let externalOutputPersisted = false;
1066
1077
  // Native runs have runPiStreaming write outputFile live; external runs persist it
1067
1078
  // before their surface can be destructively closed as delivered.
1068
1079
  if (externalOwner) {
1069
1080
  try {
1070
1081
  fs.writeFileSync(ctx.outputFile, outputForSummary, "utf-8");
1082
+ externalOutputPersisted = true;
1071
1083
  } catch (error) {
1072
1084
  const message = safeExternalFailure("output persistence", error);
1073
1085
  effectiveFinalExitCode = 1;
@@ -1090,6 +1102,30 @@ export async function runSingleStep(
1090
1102
  executionSurface = { handle: finishResult.handle, retained: finishResult.retained };
1091
1103
  }
1092
1104
 
1105
+ if (step.outputMode === "file-only"
1106
+ && (effectiveFinalExitCode !== 0 || resolvedOutput.saveError)
1107
+ && !blockedLine(effectiveFinalError ?? "")) {
1108
+ const pointer = formatFileOnlyFailure({
1109
+ outputPath: step.outputPath,
1110
+ saveError: resolvedOutput.saveError,
1111
+ ...(preservedAt !== undefined ? { preservedAt } : { notPreservedReason }),
1112
+ });
1113
+ outputForSummary = attemptNotes.length > 0
1114
+ ? `${attemptNotes.join("\n")}\n\n${pointer}`
1115
+ : pointer;
1116
+ }
1117
+
1118
+ // finish can turn a saved success into a failure after the pre-close log write.
1119
+ if (externalOutputPersisted && outputForSummary !== outputBeforeExternalFinalization) {
1120
+ try {
1121
+ fs.writeFileSync(ctx.outputFile, outputForSummary, "utf-8");
1122
+ } catch (error) {
1123
+ const message = safeExternalFailure("output persistence", error);
1124
+ effectiveFinalExitCode = 1;
1125
+ effectiveFinalError = effectiveFinalError ? `${effectiveFinalError}\n${message}` : message;
1126
+ }
1127
+ }
1128
+
1093
1129
  return {
1094
1130
  agent: step.agent,
1095
1131
  output: outputForSummary,
@@ -46,7 +46,7 @@ import { PINNED_AGENT_DIR_OVERRIDE } from "../../shared/agent-dir.ts";
46
46
  import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
47
47
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
48
48
  import { buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
49
- import { captureSingleOutputSnapshot, validateFileOnlyOutputMode } from "../shared/single-output.ts";
49
+ import { captureSingleOutputSnapshot, formatFileOnlyFailure, validateFileOnlyOutputMode } from "../shared/single-output.ts";
50
50
  import {
51
51
  buildModelCandidates,
52
52
  formatModelAttemptNote,
@@ -815,6 +815,8 @@ export async function runSync(
815
815
  } satisfies SingleResult;
816
816
 
817
817
  let postLoopError: unknown;
818
+ let preservedAt: string | undefined;
819
+ let notPreservedReason = "artifact output disabled";
818
820
  try {
819
821
  result.usage = aggregateUsage;
820
822
  result.attemptedModels = attemptedModels.length > 0 ? attemptedModels : undefined;
@@ -834,7 +836,14 @@ export async function runSync(
834
836
  if (artifactPathsResult && options.artifactConfig?.enabled !== false) {
835
837
  result.artifactPaths = artifactPathsResult;
836
838
  if (options.artifactConfig?.includeOutput !== false) {
837
- writeArtifact(artifactPathsResult.outputPath, artifactOutputByResult.get(result) ?? result.finalOutput ?? "");
839
+ try {
840
+ writeArtifact(artifactPathsResult.outputPath, artifactOutputByResult.get(result) ?? result.finalOutput ?? "");
841
+ preservedAt = artifactPathsResult.outputPath;
842
+ } catch (error) {
843
+ const message = error instanceof Error ? error.message : String(error);
844
+ notPreservedReason = `artifact write failed: ${message}`;
845
+ throw error;
846
+ }
838
847
  }
839
848
  if (options.artifactConfig?.includeMetadata !== false) {
840
849
  writeMetadata(artifactPathsResult.metadataPath, {
@@ -912,9 +921,19 @@ export async function runSync(
912
921
  : safeMessage;
913
922
  result.executionSurface ??= { handle: externalOwner.surface, retained: true };
914
923
  }
915
- } else if (postLoopError) {
916
- throw postLoopError;
917
924
  }
918
925
 
926
+ if (options.outputMode === "file-only"
927
+ && (result.exitCode !== 0 || result.outputSaveError)
928
+ && !blockedLine(result.error ?? "")) {
929
+ result.finalOutput = formatFileOnlyFailure({
930
+ outputPath: options.outputPath,
931
+ saveError: result.outputSaveError,
932
+ ...(preservedAt !== undefined ? { preservedAt } : { notPreservedReason }),
933
+ });
934
+ result.truncation = undefined;
935
+ }
936
+ if (!externalOwner && postLoopError) throw postLoopError;
937
+
919
938
  return result;
920
939
  }
@@ -39,6 +39,7 @@ import { createForkContextResolver } from "../../shared/fork-context.ts";
39
39
  import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
40
40
  import { formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts";
41
41
  import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeTopLevelOutput, resolveParallelTaskOutputPath, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
42
+ import { blockedLine } from "../shared/completion-guard.ts";
42
43
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd } from "../../shared/utils.ts";
43
44
  import { resolveSubagentResultStatus } from "../shared/result-children.ts";
44
45
  import { buildRevivedAsyncTask, resolveAsyncResumeTarget } from "../background/async-resume.ts";
@@ -2064,7 +2065,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2064
2065
  if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths);
2065
2066
 
2066
2067
  const fullOutput = getSingleResultOutput(r);
2067
- const finalizedOutput = finalizeSingleOutput({
2068
+ const displayOutput = r.outputMode === "file-only" ? r.finalOutput : finalizeSingleOutput({
2068
2069
  fullOutput,
2069
2070
  truncatedOutput: r.truncation?.text,
2070
2071
  outputPath,
@@ -2073,7 +2074,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2073
2074
  savedPath: r.savedOutputPath,
2074
2075
  outputReference: r.outputReference,
2075
2076
  saveError: r.outputSaveError,
2076
- });
2077
+ }).displayOutput;
2077
2078
  const details = compactForegroundDetails({
2078
2079
  mode: "single",
2079
2080
  runId,
@@ -2096,14 +2097,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2096
2097
  };
2097
2098
  }
2098
2099
 
2099
- if (r.exitCode !== 0)
2100
- return {
2101
- content: [{ type: "text", text: r.error || "Failed" }],
2102
- details,
2103
- isError: true,
2104
- };
2100
+ if (r.exitCode !== 0) {
2101
+ const text = r.outputMode === "file-only" && !blockedLine(r.error ?? "")
2102
+ ? `FAILED (exit code ${r.exitCode})${r.error ? `: ${r.error}` : ""}\n\n${r.finalOutput ?? ""}`
2103
+ : r.error || "Failed";
2104
+ return { content: [{ type: "text", text }], details, isError: true };
2105
+ }
2105
2106
  return {
2106
- content: [{ type: "text", text: finalizedOutput.displayOutput || "(no output)" }],
2107
+ content: [{ type: "text", text: displayOutput || "(no output)" }],
2107
2108
  details,
2108
2109
  };
2109
2110
  }
@@ -106,6 +106,16 @@ export function formatSavedOutputReference(savedPath: string, fullOutput: string
106
106
  };
107
107
  }
108
108
 
109
+ export function formatFileOnlyFailure(params: {
110
+ outputPath?: string;
111
+ saveError?: string;
112
+ } & ({ preservedAt: string; notPreservedReason?: never } | { preservedAt?: never; notPreservedReason: string })): string {
113
+ const pointer = params.preservedAt !== undefined
114
+ ? `Full reply preserved at ${params.preservedAt}`
115
+ : `Full reply not preserved (${params.notPreservedReason})`;
116
+ return params.saveError ? `Output file error: ${params.outputPath}\n${params.saveError}\n\n${pointer}` : pointer;
117
+ }
118
+
109
119
  export function validateFileOnlyOutputMode(outputMode: OutputMode | undefined, outputPath: string | undefined, context: string): string | undefined {
110
120
  if (outputMode === "file-only" && !outputPath) {
111
121
  return `${context} sets outputMode: "file-only" but does not configure an output file. Set output to a path or use outputMode: "inline".`;