From dc1258d0bd73c1236c5f444e00ac8752c4e501d8 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Wed, 28 Oct 2026 20:15:20 -0700 Subject: [PATCH] fix(monitor): an undecided not_saved waits for its run's record (#247, #90) A not_saved from the ladder's undecided step is not final before a record exists: the recorded saver may still resolve another branch. The settle lookup now keeps such a file waiting; with the record it stores nothing and logs the opted-out line. An early not_saved (a switched-off node upstream of any sampler's positive) never reaches the lookup and stays final. Tests: the undecided case (literal and wired switch) waits, then skips; a switched-off node feeding one branch of a shared-prefix run never waits and stores nothing, not even the branch the recorded saver names. --- tests/test_foreign_save_to_library.py | 52 ++++++++++++++++++++++++--- utils/image_monitor.py | 8 ++--- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/tests/test_foreign_save_to_library.py b/tests/test_foreign_save_to_library.py index ee2d9478..25de2173 100644 --- a/tests/test_foreign_save_to_library.py +++ b/tests/test_foreign_save_to_library.py @@ -153,15 +153,57 @@ class TestForeignPathKeepsSaveToLibrary(attribution.RecordedRuns, unittest.TestC self.assertIn(wired, self.recheck(path)) self.assert_nothing_stored(path) - def test_q3_not_saved_after_the_settle_lookup_ends_the_wait(self): - """No record yet and the answer is a switched-off node's text: the - file never waits for a seal that could only store nothing.""" - self.metadata = {"prompt": off_text_unwired()} - path = self.image("vendor_00001_.png") + def test_q3_an_undecided_not_saved_waits_for_the_record_then_skips(self): + """No record yet: a not_saved of the undecided step is not final (a + recorded saver may resolve another branch), so the file waits for its + run; the decision with the record stores nothing and logs the skip.""" + wired = off_text_unwired() + wired["40"]["inputs"]["save_to_library"] = ["20", 0] + wired["20"] = {"class_type": "SomeBoolNode", "inputs": {}} + cases = ( + ("off", off_text_unwired(), SKIPPED), + ("wired", wired, "Save to library is wired from a node"), + ) + for name, graph, line in cases: + with self.subTest(name=name): + self.metadata = {"prompt": graph} + path = self.image(f"vendor_{name}_00001_.png") + self.handler.process_new_image(path) + self.assertIsNone(self.linked_text(path)) + self.assertEqual(self.handler.awaiting_foreign(), [self.key(path)]) + with self.assertLogs(LOGGER, "INFO") as logs: + self.seal(path) + self.assertTrue( + self.wait_for(lambda: not self.handler.awaiting_foreign()) + ) + self.assertTrue(self.settled()) + self.assertIn(line, "\n".join(logs.output)) + self.assert_nothing_stored(path) + + def test_q3_an_early_not_saved_never_waits_even_with_a_saver_branch(self): + """Review: a switched-off node feeding one branch's positive is the + ladder's early, final not_saved (PR 1's #90 rule covers every + sampler's positive), so the file never waits and nothing is stored, + not even the plain branch the recorded saver names.""" + graph = shapes.shared_prefix() + graph["20"]["inputs"]["text"] = ["50", 0] + graph["50"] = { + "class_type": "PromptManagerText", + "inputs": {"text": OFF_TEXT, "save_to_library": False}, + } + self.metadata = {"prompt": graph} + path = self.image("IMG_GEN_00001_.png") with self.assertLogs(LOGGER, "INFO") as logs: self.handler.process_new_image(path) self.assertIn(SKIPPED, "\n".join(logs.output)) self.assert_nothing_stored(path) + self.seal(path, nodes=("13",)) + self.assertTrue(self.settled()) + self.assertIn(SKIPPED, self.recheck(path)) + self.assert_nothing_stored(path) + self.assertIsNone( + self.db.get_prompt_by_hash(generate_prompt_hash(shapes.BRANCH_TEXTS[0])) + ) def test_q4_an_off_node_on_the_negative_only_still_links_the_positive(self): positive = "a lighthouse on a cliff at dusk" diff --git a/utils/image_monitor.py b/utils/image_monitor.py index aa36a3f0..d2fc1c19 100644 --- a/utils/image_monitor.py +++ b/utils/image_monitor.py @@ -804,8 +804,10 @@ class ImageGenerationHandler(FileSystemEventHandler): Registered first, so a run sealed before this lookup is not missed. Returns None to keep waiting: no record yet, and the answer is not the inspector's or the recorded saver's (the legacy floor waits for - the re-check). With a record, any decision ends the wait, and so - does a switched-off node's text (#90): a record could only store it. + the re-check). With a record, any decision ends the wait. A + ``not_saved`` here is the undecided step's (an early one never asks + history, so it never reaches this lookup): a recorded saver may still + resolve another branch, so without a record it waits too (#90). """ self._await_foreign(image_path) found = [] @@ -818,8 +820,6 @@ class ImageGenerationHandler(FileSystemEventHandler): decision = decide_foreign(image_path, metadata, in_output=True, lookup=recorded) if any(record is not None for record in found): return decision - if decision.kind in NOT_SAVED_KINDS: - return decision if decision.kind in CERTAIN and decision.source in ANCHORED_SOURCES: return decision return None