Erinaldordso commited on
Commit
6a39baa
·
verified ·
1 Parent(s): 2730c2f

fix: bound the waits that could freeze the learn loop (voyager_stack.py)

Browse files
Files changed (1) hide show
  1. voyager_stack.py +147 -0
voyager_stack.py CHANGED
@@ -793,6 +793,153 @@ async function exploreUntil(bot, direction, maxTime = 60, callback) {
793
  action_text = action_text.replace(marker, creative_facts + "\n" + marker, 1)
794
  action_prompt.write_text(action_text, encoding="utf-8")
795
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
796
  action_agent_path = SRC / "voyager" / "agents" / "action.py"
797
  action_agent_text = action_agent_path.read_text(encoding="utf-8")
798
  action_agent_text = action_agent_text.replace(
 
793
  action_text = action_text.replace(marker, creative_facts + "\n" + marker, 1)
794
  action_prompt.write_text(action_text, encoding="utf-8")
795
 
796
+ # SkillManager.__init__ built Chroma() with no deadline at all, and
797
+ # Chroma embeds through OpenAIEmbeddings(), which in langchain 0.0.350
798
+ # calls openai==0.28.1 with no request timeout. A single stalled
799
+ # /v1/embeddings response therefore parked the constructor forever:
800
+ # the log stopped at "Loading Skill Manager" and the loop never reached
801
+ # learn(), so nothing failed, nothing retried and no counter moved.
802
+ # The stall watchdog then fired every 900s and looked like the cause
803
+ # instead of the symptom.
804
+ #
805
+ # The same constructor ended with a hard assert. A vectordb that is
806
+ # one document out of sync is recoverable -- the skills.json on disk is
807
+ # the source of truth -- but the assert turned that into a dead process.
808
+ # Rebuild the index from skills.json instead, which is what the comment
809
+ # in the failure message already told the operator to do by hand.
810
+ skill_path = SRC / "voyager" / "agents" / "skill.py"
811
+ skill_text = skill_path.read_text(encoding="utf-8")
812
+ old_vectordb = ''' self.vectordb = Chroma(
813
+ collection_name="skill_vectordb",
814
+ embedding_function=OpenAIEmbeddings(),
815
+ persist_directory=f"{ckpt_dir}/skill/vectordb",
816
+ )'''
817
+ new_vectordb = ''' self.vectordb = self._open_vectordb(ckpt_dir)'''
818
+ if old_vectordb in skill_text:
819
+ skill_text = skill_text.replace(old_vectordb, new_vectordb, 1)
820
+ elif new_vectordb.strip() not in skill_text:
821
+ # The anchor is gone and the replacement is not already in place:
822
+ # the file changed underneath this patch and the unbounded
823
+ # constructor would survive. Refuse rather than boot a tree that
824
+ # can still hang.
825
+ raise RuntimeError("SkillManager patch anchor missing; refusing unbounded-Chroma patch")
826
+
827
+ old_assert = ''' assert self.vectordb._collection.count() == len(self.skills), (
828
+ f"Skill Manager's vectordb is not synced with skills.json.\\n"
829
+ f"There are {self.vectordb._collection.count()} skills in vectordb but {len(self.skills)} skills in skills.json.\\n"
830
+ f"Did you set resume=False when initializing the manager?\\n"
831
+ f"You may need to manually delete the vectordb directory for running from scratch."
832
+ )'''
833
+ new_resync = ''' self._resync_vectordb()'''
834
+ if old_assert in skill_text:
835
+ skill_text = skill_text.replace(old_assert, new_resync, 1)
836
+ elif "self._resync_vectordb()" not in skill_text:
837
+ raise RuntimeError("SkillManager sync-assert anchor missing; refusing patch")
838
+
839
+ skill_helpers = ''' # Every embedding this class makes goes through here, so one stalled
840
+ # response cannot freeze the constructor, add_new_skill or retrieve_skills.
841
+ EMBED_DEADLINE_SECONDS = 60
842
+ VECTORDB_OPEN_SECONDS = 180
843
+
844
+ def _embedding_function(self):
845
+ """OpenAIEmbeddings with an explicit timeout.
846
+
847
+ langchain 0.0.350 builds the openai 0.28.1 client, whose
848
+ Embedding.create has no default request timeout. The base URL is the
849
+ remote brain Space, so a slow or wedged response is a real possibility
850
+ and must surface as an exception the runner can retry, not as a hang.
851
+
852
+ request_timeout is a declared pydantic field in this version, so pass it
853
+ to the constructor. Some versions renamed it to timeout, and a pydantic
854
+ model raises on an unknown keyword, so fall back rather than crash the
855
+ constructor over an observability nicety.
856
+ """
857
+ try:
858
+ return OpenAIEmbeddings(request_timeout=self.EMBED_DEADLINE_SECONDS)
859
+ except Exception:
860
+ try:
861
+ return OpenAIEmbeddings(timeout=self.EMBED_DEADLINE_SECONDS)
862
+ except Exception:
863
+ return OpenAIEmbeddings()
864
+
865
+ def _open_vectordb(self, ckpt_dir):
866
+ """Construct Chroma under a wall-clock deadline.
867
+
868
+ Chroma can touch the embedding function while opening (and will, when
869
+ it has to write a missing index). Running the open in a worker gives
870
+ the constructor a deadline it did not have; the caller follows up with
871
+ _resync_vectordb, which repairs any index that is out of date.
872
+ """
873
+ import concurrent.futures as _cf
874
+
875
+ def _open():
876
+ return Chroma(
877
+ collection_name="skill_vectordb",
878
+ embedding_function=self._embedding_function(),
879
+ persist_directory=f"{ckpt_dir}/skill/vectordb",
880
+ )
881
+
882
+ with _cf.ThreadPoolExecutor(max_workers=1) as pool:
883
+ future = pool.submit(_open)
884
+ try:
885
+ return future.result(timeout=self.VECTORDB_OPEN_SECONDS)
886
+ except _cf.TimeoutError as exc:
887
+ raise RuntimeError(
888
+ "vector store did not open within "
889
+ f"{self.VECTORDB_OPEN_SECONDS}s; the embedding endpoint is "
890
+ "not answering"
891
+ ) from exc
892
+
893
+ def _resync_vectordb(self):
894
+ """Make the index match skills.json, which is the source of truth.
895
+
896
+ Replaces a hard assert that killed the process when the two counts
897
+ disagreed by one document. Rebuilding is safe: every skill is present
898
+ in skills.json with its code and description.
899
+ """
900
+ expected = set(self.skills)
901
+ try:
902
+ stored = set(self.vectordb._collection.get()["ids"])
903
+ except Exception:
904
+ stored = set()
905
+ for stale in stored - expected:
906
+ try:
907
+ self.vectordb._collection.delete(ids=[stale])
908
+ except Exception:
909
+ pass
910
+ missing = expected - stored
911
+ if missing:
912
+ print(
913
+ f"\\033[33mSkill Manager resyncing {len(missing)} skill(s) into the vectordb\\033[0m",
914
+ flush=True,
915
+ )
916
+ for name in sorted(missing):
917
+ entry = self.skills.get(name) or {}
918
+ try:
919
+ self.vectordb.add_texts(
920
+ texts=[f"Skill: {name}\\n{entry.get('description', '')}"],
921
+ metadatas=[{"name": name}],
922
+ ids=[name],
923
+ )
924
+ except Exception as exc:
925
+ print(
926
+ f"\\033[33mSkill Manager could not index {name}: "
927
+ f"{type(exc).__name__}: {exc}\\033[0m",
928
+ flush=True,
929
+ )
930
+ try:
931
+ self.vectordb.persist()
932
+ except Exception:
933
+ pass
934
+
935
+ '''
936
+ if "def _open_vectordb" not in skill_text:
937
+ marker_after = " @property\n def programs(self):"
938
+ if marker_after not in skill_text:
939
+ raise RuntimeError("SkillManager property anchor missing; refusing patch")
940
+ skill_text = skill_text.replace(marker_after, skill_helpers + marker_after, 1)
941
+ skill_path.write_text(skill_text, encoding="utf-8")
942
+
943
  action_agent_path = SRC / "voyager" / "agents" / "action.py"
944
  action_agent_text = action_agent_path.read_text(encoding="utf-8")
945
  action_agent_text = action_agent_text.replace(