Commit bf8a8301b8847fe2fbe2023bbb875c1f9b9e82f6
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>
Showing
2 changed files
with
33 additions
and
1 deletions
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( |