fix(copilot): sandbox kill, tool event logging, and background task UX

- Fix sandbox process kill: use start_new_session + os.killpg to kill
  the entire bwrap process group on timeout (proc.kill alone only kills
  the parent, leaving children running until natural completion)
- Add StreamToolInputAvailable/StreamToolOutputAvailable to publish_chunk
  logging filter so tool events are visible in Docker logs
- Add system prompt instruction telling Claude not to use
  run_in_background on Task tool (gets denied by security hooks)
- Add tool event debug logging in SDK streaming loop for tracing
  tool execution visibility issues
This commit is contained in:
Zamil Majdy
2026-02-20 02:24:31 +07:00
parent 23225aa323
commit a0a040f102
3 changed files with 30 additions and 2 deletions

View File

@@ -100,6 +100,8 @@ _SDK_TOOL_SUPPLEMENT = """
- Long-running tools (create_agent, edit_agent, etc.) are handled
asynchronously. You will receive an immediate response; the actual result
is delivered to the user via a background stream.
- When using the Task tool, NEVER set `run_in_background` to true.
All tasks must run in the foreground.
"""
@@ -680,6 +682,17 @@ async def stream_chat_completion_sdk(
if isinstance(response, StreamStart):
continue
# Log tool events for debugging visibility issues
if isinstance(
response,
(StreamToolInputAvailable, StreamToolOutputAvailable),
):
logger.info(
"[SDK] Tool event: %s, tool=%s",
type(response).__name__,
getattr(response, "toolName", "N/A"),
)
yield response
if isinstance(response, StreamTextDelta):

View File

@@ -227,7 +227,14 @@ async def publish_chunk(
# Only log timing for significant chunks or slow operations
if (
chunk_type
in ("StreamStart", "StreamFinish", "StreamTextStart", "StreamTextEnd")
in (
"StreamStart",
"StreamFinish",
"StreamTextStart",
"StreamTextEnd",
"StreamToolInputAvailable",
"StreamToolOutputAvailable",
)
or total_time > 50
):
logger.info(

View File

@@ -13,6 +13,7 @@ import logging
import os
import platform
import shutil
import signal
logger = logging.getLogger(__name__)
@@ -245,6 +246,7 @@ async def run_sandboxed(
stderr=asyncio.subprocess.PIPE,
cwd=cwd,
env=safe_env,
start_new_session=True, # Own process group for clean kill
)
try:
@@ -255,7 +257,13 @@ async def run_sandboxed(
stderr = stderr_bytes.decode("utf-8", errors="replace")
return stdout, stderr, proc.returncode or 0, False
except asyncio.TimeoutError:
proc.kill()
# Kill entire process group (bwrap + all children).
# proc.kill() alone only kills the bwrap parent, leaving
# children running until they finish naturally.
try:
os.killpg(proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass # Already exited
await proc.communicate()
return "", f"Execution timed out after {timeout}s", -1, True