|
31 | 31 | # find_active_loop default ignores marker -> validators stay isolated |
32 | 32 | # hook input omits session_id -> cross-session guard fires |
33 | 33 | # malformed transcript with marker -> marker preserved (fail-closed) |
| 34 | +# TaskStop scan with non-string message -> unrelated tool_result does not poison completions |
34 | 35 | # |
35 | 36 |
|
36 | 37 | set -euo pipefail |
@@ -315,6 +316,25 @@ emit_task_stop_result_message_only() { |
315 | 316 | }' |
316 | 317 | } |
317 | 318 |
|
| 319 | +# Unrelated tool_result whose .message is a structured value rather than a |
| 320 | +# string. list_pending_background_task_ids scans every .toolUseResult while |
| 321 | +# looking for TaskStop records; a non-string .message must not make jq's |
| 322 | +# `contains()` error and poison the completion pipeline under pipefail. |
| 323 | +emit_tool_result_nonstring_message() { |
| 324 | + local tool_use_id="$1" |
| 325 | + jq -c -n \ |
| 326 | + --arg id "$tool_use_id" \ |
| 327 | + '{ |
| 328 | + type:"user", |
| 329 | + message:{ |
| 330 | + role:"user", |
| 331 | + content:[{tool_use_id:$id, type:"tool_result", |
| 332 | + content:[{type:"text", text:"structured result"}]}] |
| 333 | + }, |
| 334 | + toolUseResult:{message:{status:"ok", details:[1,2,3]}, status:"success"} |
| 335 | + }' |
| 336 | +} |
| 337 | + |
318 | 338 | write_transcript() { |
319 | 339 | local path="$1" |
320 | 340 | shift |
@@ -1670,6 +1690,34 @@ else |
1670 | 1690 | "empty pending list" "got: $TASKSTOP_MSGONLY_HELPER_PENDING" |
1671 | 1691 | fi |
1672 | 1692 |
|
| 1693 | +# ---- TaskStop scan tolerates structured messages on unrelated tool_results ---- |
| 1694 | +# Source 3 iterates every .toolUseResult and calls `contains()` on .message. |
| 1695 | +# An unrelated tool_result whose .message is an object/array/number would make |
| 1696 | +# jq error; because the completion pipeline falls back to `completed=""`, that |
| 1697 | +# error wipes valid SDK/legacy completions and makes completed tasks pending |
| 1698 | +# again. The fix coerces .message to a string before `contains()`. |
| 1699 | +echo "Test: helper tolerates non-string .message on unrelated tool_results" |
| 1700 | +NONSTRING_MSG_REPO="$TEST_DIR/nonstring_msg" |
| 1701 | +create_full_fixture "$NONSTRING_MSG_REPO" > /dev/null |
| 1702 | +NONSTRING_MSG_TRANSCRIPT="$TRANSCRIPTS_DIR/nonstring_msg.jsonl" |
| 1703 | +NONSTRING_MSG_LAUNCH=$(emit_tool_use_assistant "toolu_nonstring" "Agent" ',"description":"x","prompt":"x"') |
| 1704 | +NONSTRING_MSG_RESULT=$(emit_async_agent_launch_result "toolu_nonstring" "agent_nonstring") |
| 1705 | +NONSTRING_MSG_COMPLETE=$(emit_sdk_task_notification "agent_nonstring" "toolu_nonstring" "completed") |
| 1706 | +NONSTRING_MSG_NOISE=$(emit_tool_result_nonstring_message "toolu_noise") |
| 1707 | +write_transcript "$NONSTRING_MSG_TRANSCRIPT" "$NONSTRING_MSG_LAUNCH" "$NONSTRING_MSG_RESULT" "$NONSTRING_MSG_COMPLETE" "$NONSTRING_MSG_NOISE" |
| 1708 | + |
| 1709 | +NONSTRING_MSG_PENDING=$( |
| 1710 | + # shellcheck source=/dev/null |
| 1711 | + source "$PROJECT_ROOT/hooks/lib/loop-common.sh" |
| 1712 | + list_pending_background_task_ids "$NONSTRING_MSG_TRANSCRIPT" 2>/dev/null |
| 1713 | +) |
| 1714 | +if [[ -z "$NONSTRING_MSG_PENDING" ]]; then |
| 1715 | + pass "non-string .message on unrelated tool_result does not poison completions" |
| 1716 | +else |
| 1717 | + fail "non-string .message on unrelated tool_result does not poison completions" \ |
| 1718 | + "empty pending list" "got: $NONSTRING_MSG_PENDING" |
| 1719 | +fi |
| 1720 | + |
1673 | 1721 | # ---- cross-session output glob: dead task pruned when transcript has no path ---- |
1674 | 1722 | # Fix B - cross-session output glob. On session resume the current |
1675 | 1723 | # transcript has a NEW session id while the task's .output file lives |
|
0 commit comments