@ryan_nookpi/pi-extension-subagent 0.5.11 → 0.5.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/lifecycle.ts +7 -0
  2. package/package.json +1 -1
  3. package/runner.ts +103 -21
package/lifecycle.ts CHANGED
@@ -31,6 +31,13 @@ function reconcileRunWithPersistedSession(run: CommandRunState): void {
31
31
  run.lastActivityAt = snapshot.latestActivityAt;
32
32
  }
33
33
  if (!snapshot.isTerminal) return;
34
+ // A live Pi child can still retry a persisted error. Only its runner's
35
+ // completion marker proves that failure recovery has finished.
36
+ if (
37
+ !snapshot.completionMarker &&
38
+ (snapshot.terminalStopReason === "error" || snapshot.terminalStopReason === "aborted")
39
+ )
40
+ return;
34
41
 
35
42
  const exitCode =
36
43
  snapshot.completionMarker?.exitCode ??
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryan_nookpi/pi-extension-subagent",
3
- "version": "0.5.11",
3
+ "version": "0.5.12",
4
4
  "description": "Asynchronous subagent delegation for pi with run, batch, chain, and continuation workflows.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/runner.ts CHANGED
@@ -762,6 +762,9 @@ async function runPiAgent(
762
762
  let lastProcessSignal: NodeJS.Signals | null = null;
763
763
  let lastEventAt = Date.now();
764
764
  let sawAgentEnd = false;
765
+ // Covers both backoff and the resumed attempt, until Pi reports recovery
766
+ // or a final agent_end. Raw history can retain errors throughout this time.
767
+ let retryPending = false;
765
768
  let settleReason = "unknown";
766
769
  let unparsedStdoutCount = 0;
767
770
  const unparsedStdoutTail: string[] = [];
@@ -813,10 +816,24 @@ async function runPiAgent(
813
816
  }, 5000);
814
817
  };
815
818
 
819
+ const clearCompletionFallbacks = () => {
820
+ if (agentEndFallbackTimer) clearTimeout(agentEndFallbackTimer);
821
+ if (terminalMessageFallbackTimer) clearTimeout(terminalMessageFallbackTimer);
822
+ agentEndFallbackTimer = undefined;
823
+ terminalMessageFallbackTimer = undefined;
824
+ };
825
+
816
826
  const syncFromPersistedSession = (allowResolve: boolean): boolean => {
817
827
  if (!sessionFile) return false;
818
828
  const snapshot = readPersistedSessionSnapshot(sessionFile, { startOffset: persistedSessionBaseOffset });
819
- if (snapshot.terminalStopReason && !currentResult.stopReason) {
829
+ const unconfirmedFailure =
830
+ !snapshot.completionMarker &&
831
+ (snapshot.terminalStopReason === "error" || snapshot.terminalStopReason === "aborted");
832
+ if (
833
+ snapshot.terminalStopReason &&
834
+ !currentResult.stopReason &&
835
+ (procExited || settled || (!retryPending && !unconfirmedFailure))
836
+ ) {
820
837
  currentResult.stopReason = snapshot.terminalStopReason;
821
838
  }
822
839
  if (snapshot.messages.length > currentResult.messages.length) {
@@ -824,7 +841,18 @@ async function runPiAgent(
824
841
  currentResult.liveText = undefined;
825
842
  emitUpdate();
826
843
  }
827
- if (!allowResolve || !snapshot.isTerminal || settled || procExited || wasAborted) return false;
844
+ // Persistence can beat stdout. An error response alone is not proof
845
+ // that Pi has finished: agent_end/auto_retry_start decide its recovery.
846
+ if (
847
+ !allowResolve ||
848
+ !snapshot.isTerminal ||
849
+ settled ||
850
+ procExited ||
851
+ wasAborted ||
852
+ retryPending ||
853
+ unconfirmedFailure
854
+ )
855
+ return false;
828
856
 
829
857
  const forcedCode =
830
858
  snapshot.completionMarker?.exitCode ??
@@ -855,15 +883,44 @@ async function runPiAgent(
855
883
  return;
856
884
  }
857
885
  lastEventAt = Date.now();
886
+ // Once the local context guard settles the run, late shutdown events
887
+ // must not replace its failure with the child's last toolUse response.
888
+ if (contextGuardTripped) return;
889
+
890
+ if (event.type === "auto_retry_start") {
891
+ retryPending = true;
892
+ sawAgentEnd = false;
893
+ clearCompletionFallbacks();
894
+ return;
895
+ }
896
+
897
+ if (event.type === "auto_retry_end") {
898
+ retryPending = false;
899
+ if (event.success === false) {
900
+ currentResult.stopReason = "error";
901
+ if (event.finalError) currentResult.errorMessage = event.finalError;
902
+ sawAgentEnd = true;
903
+ writeCompletionMarkerOnce(1);
904
+ scheduleAgentEndForceResolve();
905
+ } else if (currentResult.stopReason === "error" || currentResult.stopReason === "aborted") {
906
+ // Successful recovery may arrive without its message_end on stdout.
907
+ // Wait for the successful agent_end instead of reusing the old error.
908
+ currentResult.stopReason = undefined;
909
+ currentResult.errorMessage = undefined;
910
+ } else if (currentResult.stopReason && currentResult.stopReason !== "toolUse") {
911
+ writeCompletionMarkerOnce(0);
912
+ scheduleTerminalMessageForceResolve();
913
+ }
914
+ return;
915
+ }
858
916
 
859
917
  if (event.type === "agent_start" || event.type === "turn_start") {
860
918
  sawAgentEnd = false;
919
+ currentResult.stopReason = undefined;
920
+ currentResult.errorMessage = undefined;
861
921
  currentResult.liveThinking = undefined;
862
922
  currentResult.thoughtText = undefined;
863
- if (terminalMessageFallbackTimer) {
864
- clearTimeout(terminalMessageFallbackTimer);
865
- terminalMessageFallbackTimer = undefined;
866
- }
923
+ clearCompletionFallbacks();
867
924
  return;
868
925
  }
869
926
 
@@ -873,21 +930,32 @@ async function runPiAgent(
873
930
  // its objects are freshly deserialized, so reference-based deduplication
874
931
  // would append every prior message again.
875
932
  const eventMessages = (event.messages ?? []) as Message[];
876
- const terminalMessage = [...eventMessages].reverse().find((msg) => Boolean((msg as any).stopReason));
933
+ const terminalMessage = [...eventMessages]
934
+ .reverse()
935
+ .find((msg) => msg.role === "assistant" && Boolean(msg.stopReason));
877
936
  if (terminalMessage) {
878
937
  const fingerprint = JSON.stringify(terminalMessage);
879
938
  const alreadyCaptured = currentResult.messages.some((msg) => JSON.stringify(msg) === fingerprint);
880
939
  if (!alreadyCaptured) currentResult.messages.push(terminalMessage);
881
- if (!currentResult.stopReason) {
882
- currentResult.stopReason = (terminalMessage as any).stopReason;
883
- if ((terminalMessage as any).errorMessage) {
884
- currentResult.errorMessage = (terminalMessage as any).errorMessage;
885
- }
886
- }
940
+ currentResult.stopReason = (terminalMessage as any).stopReason;
941
+ currentResult.errorMessage = (terminalMessage as any).errorMessage;
887
942
  } else if (currentResult.messages.length === 0 && eventMessages.length > 0) {
888
943
  currentResult.messages.push(eventMessages[eventMessages.length - 1]);
889
944
  }
890
- if (currentResult.stopReason && currentResult.stopReason !== "toolUse") {
945
+ if (event.willRetry === true) {
946
+ retryPending = true;
947
+ sawAgentEnd = false;
948
+ clearCompletionFallbacks();
949
+ return;
950
+ }
951
+ retryPending = false;
952
+ if (
953
+ currentResult.stopReason &&
954
+ currentResult.stopReason !== "toolUse" &&
955
+ // Older Pi versions omit willRetry; defer their failure marker
956
+ // until fallback so auto_retry_start can still cancel completion.
957
+ (currentResult.stopReason !== "error" || event.willRetry === false)
958
+ ) {
891
959
  writeCompletionMarkerOnce(
892
960
  currentResult.stopReason === "error" || currentResult.stopReason === "aborted" ? 1 : 0,
893
961
  );
@@ -977,7 +1045,7 @@ async function runPiAgent(
977
1045
  }
978
1046
  if (!currentResult.model && msg.model) currentResult.model = msg.model;
979
1047
  if (msg.stopReason) currentResult.stopReason = msg.stopReason;
980
- if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
1048
+ currentResult.errorMessage = msg.errorMessage;
981
1049
  currentResult.liveThinking = undefined;
982
1050
 
983
1051
  // Extract thoughtText from thinking block first line only
@@ -989,8 +1057,15 @@ async function runPiAgent(
989
1057
  }
990
1058
  }
991
1059
  const terminalStopReason = (msg as any).stopReason;
992
- if (terminalStopReason && terminalStopReason !== "toolUse") {
993
- writeCompletionMarkerOnce(terminalStopReason === "error" || terminalStopReason === "aborted" ? 1 : 0);
1060
+ // Error message_end precedes Pi's retry decision. Do not publish
1061
+ // completion or arm a timeout while it may still recover.
1062
+ if (
1063
+ terminalStopReason &&
1064
+ terminalStopReason !== "toolUse" &&
1065
+ terminalStopReason !== "error" &&
1066
+ !retryPending
1067
+ ) {
1068
+ writeCompletionMarkerOnce(terminalStopReason === "aborted" ? 1 : 0);
994
1069
  scheduleTerminalMessageForceResolve();
995
1070
  }
996
1071
  }
@@ -1062,11 +1137,18 @@ async function runPiAgent(
1062
1137
  };
1063
1138
 
1064
1139
  function scheduleTerminalMessageForceResolve() {
1065
- if (!currentResult.stopReason || currentResult.stopReason === "toolUse" || settled || procExited) return;
1140
+ if (
1141
+ !currentResult.stopReason ||
1142
+ currentResult.stopReason === "toolUse" ||
1143
+ retryPending ||
1144
+ settled ||
1145
+ procExited
1146
+ )
1147
+ return;
1066
1148
  if (terminalMessageFallbackTimer) clearTimeout(terminalMessageFallbackTimer);
1067
1149
 
1068
1150
  terminalMessageFallbackTimer = setTimeout(() => {
1069
- if (settled || procExited || wasAborted) return;
1151
+ if (settled || procExited || wasAborted || retryPending) return;
1070
1152
 
1071
1153
  const forcedCode = currentResult.stopReason === "error" || currentResult.stopReason === "aborted" ? 1 : 0;
1072
1154
 
@@ -1082,12 +1164,12 @@ async function runPiAgent(
1082
1164
  // (e.g. lingering extension timers/transports). In that case, force
1083
1165
  // resolve after a short quiet period so runs do not remain "running" forever.
1084
1166
  function scheduleAgentEndForceResolve() {
1085
- if (!sawAgentEnd || settled || procExited) return;
1167
+ if (!sawAgentEnd || retryPending || settled || procExited) return;
1086
1168
  if (agentEndFallbackTimer) clearTimeout(agentEndFallbackTimer);
1087
1169
 
1088
1170
  const marker = lastEventAt;
1089
1171
  agentEndFallbackTimer = setTimeout(() => {
1090
- if (settled || procExited || wasAborted) return;
1172
+ if (settled || procExited || wasAborted || retryPending) return;
1091
1173
  if (lastEventAt !== marker) return;
1092
1174
 
1093
1175
  const forcedCode = currentResult.stopReason === "error" || currentResult.stopReason === "aborted" ? 1 : 0;