diff --git a/src/runtime/session.py b/src/runtime/session.py index 555e7ba..704ff66 100644 --- a/src/runtime/session.py +++ b/src/runtime/session.py @@ -907,8 +907,35 @@ def _classify_tool_use( elif "slack.com" in command: pass # other slack endpoints — inward housekeeping else: - bash_used = True - is_outward = True + # Detect indirect posts wrapped in helper scripts (python3 /tmp/post.py, etc.). + # If we executed a script, trace back to see if we wrote/edited that script + # containing chat.postMessage or chat.update. + is_indirect_post = False + for word in command.split(): + filename = os.path.basename(word) + if filename.endswith((".py", ".sh", ".bash")): + for prev_record in reversed(records[:i]): + if prev_record.name in ("write_file", "edit_file"): + prev_args = prev_record.input or {} + prev_path = prev_args.get("file_path") or "" + if os.path.basename(prev_path) == filename: + content = prev_args.get("content") or prev_args.get("new_string") or "" + if ("chat.postMessage" in content or "chat_postMessage" in content or + "chat.update" in content or "chat_update" in content): + is_indirect_post = True + break + elif prev_record.name == "bash": + prev_cmd = (prev_record.input or {}).get("command") or "" + if filename in prev_cmd and ("chat.postMessage" in prev_cmd or "chat.update" in prev_cmd): + is_indirect_post = True + break + if is_indirect_post: + break + if is_indirect_post: + is_post = True + else: + bash_used = True + is_outward = True elif name in ("write_file", "edit_file"): file_path = input_dict.get("file_path") or "" if not _is_journal_path(file_path): diff --git a/tests/runtime/test_silent_exit.py b/tests/runtime/test_silent_exit.py index 292e34a..1d7d398 100644 --- a/tests/runtime/test_silent_exit.py +++ b/tests/runtime/test_silent_exit.py @@ -357,3 +357,26 @@ def test_silent_exit_message_dispatches_only_on_explicit_flag(): body = msg.to_initial_user_message() assert "previous Sam session attempting to respond to a Slack message FAILED" in body assert "exited cleanly" not in body + + +# ─── Indirect Script-Based Posts ────────────────────────────────────────────── + + +def test_indirect_script_post_DOES_close_loop(): + """A session that posts by writing a script containing chat.postMessage + and executing it via bash closes the loop.""" + records = [ + ToolUseRecord(name="write_file", input={"file_path": "/tmp/post_update.py", "content": "import slack_sdk\nclient.chat_postMessage(text='PR open')"}), + _bash("python3 /tmp/post_update.py"), + ] + assert _closed_loop(records) is True + + +def test_indirect_script_post_different_file_does_NOT_close_loop(): + """If we run a python script but we didn't write/edit that file to have + a post call in this session, it does NOT close the loop.""" + records = [ + ToolUseRecord(name="write_file", input={"file_path": "/tmp/some_other.py", "content": "import slack_sdk\nclient.chat_postMessage(text='PR open')"}), + _bash("python3 /tmp/post_update.py"), # mismatch in filename + ] + assert _closed_loop(records) is False