Commit bf8a8301b8847fe2fbe2023bbb875c1f9b9e82f6

Authored by bboonnee
1 parent cac67c9a

fix: don't truncate the raw response in StructuredOutputExtractionError

Real incident (2026-08-15, task b243dfe8): this exception's message used to
truncate the failing response to [:2000] chars -- fine for the "process
exited nonzero" error paths in this same file, where a short excerpt is
enough, but actively counterproductive here: diagnosing a real production
extraction failure with only the 2000-char preview, it was impossible to
tell "the model's response was genuinely cut off mid-generation" from "this
is just where the display limit happened to land" -- both look identical
(text stopping mid-word) without the full text.

No length cap now: this flows into task_execution._mark_blocked's str(exc),
stored as a plain TEXT execution_event column with no practical size limit,
so keeping the complete response is the honest, actually-diagnosable
choice -- the same investigation that found this needed the full text and
didn't have it.

New test confirms a >2000-char failing response is preserved whole in the
exception, not truncated. 20/20 in this file; every file today's fixes
touch stays green (the only failures anywhere in the full suite are 15
pre-existing ones in a concurrent, unrelated task's own test file, confirmed
identical before and after this change).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
src/org_node_trigger.py
@@ -149,9 +149,22 @@ def trigger_org_node_structured(org_node, prompt, target_folder_path, output_sch @@ -149,9 +149,22 @@ def trigger_org_node_structured(org_node, prompt, target_folder_path, output_sch
149 required_keys = list(output_schema.get("required", [])) 149 required_keys = list(output_schema.get("required", []))
150 extracted = extract_json_payload(payload.get("result", ""), required_keys=required_keys) 150 extracted = extract_json_payload(payload.get("result", ""), required_keys=required_keys)
151 if extracted is None: 151 if extracted is None:
  152 + # 2026-08-15 (real incident, task b243dfe8): this used to truncate
  153 + # to [:2000] chars -- fine for the "process exited nonzero" error
  154 + # paths above, where a short excerpt is enough context, but actively
  155 + # counterproductive here: the WHOLE POINT of this exception is
  156 + # diagnosing why extraction failed (genuine model-side truncation
  157 + # vs some other JSON-validity issue), and a truncated preview can
  158 + # LOOK like the response was cut off mid-generation when that's
  159 + # really just this display limit -- indistinguishable from the
  160 + # real thing without the full text, which is exactly what happened
  161 + # investigating this live. No length cap: this flows into
  162 + # task_execution._mark_blocked's str(exc), stored as a plain TEXT
  163 + # execution_event column with no practical size limit, so keeping
  164 + # the complete response is the honest, actually-diagnosable choice.
152 raise StructuredOutputExtractionError( 165 raise StructuredOutputExtractionError(
153 "could not extract a payload matching required keys {} from org_node={}'s response: {!r}".format( 166 "could not extract a payload matching required keys {} from org_node={}'s response: {!r}".format(
154 - required_keys, org_node, payload.get("result", "")[:2000] 167 + required_keys, org_node, payload.get("result", "")
155 ) 168 )
156 ) 169 )
157 return extracted 170 return extracted
src/test_org_node_trigger.py
@@ -119,6 +119,25 @@ class TriggerOrgNodeStructuredTest(unittest.TestCase): @@ -119,6 +119,25 @@ class TriggerOrgNodeStructuredTest(unittest.TestCase):
119 "validator", "check this", "/tmp/some-project", _SCHEMA 119 "validator", "check this", "/tmp/some-project", _SCHEMA
120 ) 120 )
121 121
  122 + def test_extraction_error_preserves_the_full_response_not_truncated(self):
  123 + # Real incident (2026-08-15, task b243dfe8): this used to truncate
  124 + # the failing response to [:2000] chars in the exception message --
  125 + # diagnosing a real production failure with only that preview, it
  126 + # was impossible to tell "the model's response was genuinely cut
  127 + # off mid-generation" from "this is just where the 2000-char
  128 + # display limit landed" -- indistinguishable without the full text.
  129 + long_non_json_response = "not json at all, " * 200 # well over 2000 chars
  130 + self.assertGreater(len(long_non_json_response), 2000)
  131 + with patch("src.org_node_trigger.subprocess.run") as run:
  132 + run.return_value = _completed_process(
  133 + stdout_obj={"is_error": False, "result": long_non_json_response}
  134 + )
  135 + with self.assertRaises(org_node_trigger.StructuredOutputExtractionError) as ctx:
  136 + org_node_trigger.trigger_org_node_structured(
  137 + "validator", "check this", "/tmp/some-project", _SCHEMA
  138 + )
  139 + self.assertIn(long_non_json_response, str(ctx.exception))
  140 +
122 def test_appends_json_fence_instruction_and_schema_to_prompt(self): 141 def test_appends_json_fence_instruction_and_schema_to_prompt(self):
123 with patch("src.org_node_trigger.subprocess.run") as run: 142 with patch("src.org_node_trigger.subprocess.run") as run:
124 run.return_value = _completed_process( 143 run.return_value = _completed_process(