Spawn *command* in the background and register it on *context*. Returns a dict that mirrors the shape consumed by ``_bash_map_result_to_api``: it includes the background task id plus a human-readable message instructing the model how to poll the output.
(
*,
command: str,
cwd: Path,
description: str | None,
context: ToolContext,
)
| 42 | |
| 43 | |
| 44 | def spawn_background_bash( |
| 45 | *, |
| 46 | command: str, |
| 47 | cwd: Path, |
| 48 | description: str | None, |
| 49 | context: ToolContext, |
| 50 | ) -> dict[str, Any]: |
| 51 | """Spawn *command* in the background and register it on *context*. |
| 52 | |
| 53 | Returns a dict that mirrors the shape consumed by |
| 54 | ``_bash_map_result_to_api``: it includes the background task id plus a |
| 55 | human-readable message instructing the model how to poll the output. |
| 56 | """ |
| 57 | # Chapter-10 / WI-1.4: prefixed task id (``b<8 base36 chars>``) instead |
| 58 | # of the legacy ``uuid4().hex[:8]``. Mirrors TS Task.ts:79-105 — the |
| 59 | # ``b`` prefix is what TaskStop / TaskOutput dispatch on. |
| 60 | task_id = generate_task_id("local_bash") |
| 61 | output_path = _bg_output_dir() / f"{task_id}.log" |
| 62 | output_path.touch(exist_ok=True) |
| 63 | |
| 64 | output_handle = open(output_path, "wb", buffering=0) |
| 65 | |
| 66 | # Same wrapper the foreground path uses, so a trailing ``cd`` still writes |
| 67 | # the final PWD for inspection. Exit code is appended to the log after the |
| 68 | # process exits so ``TaskOutput`` can report it even if Popen.wait() races |
| 69 | # with the reader. |
| 70 | wrapped = ( |
| 71 | f"{{ {command}\n}}; __rc=$?; " |
| 72 | f"echo \"__CLAWCODEX_EXIT__=$__rc\" >&2; " |
| 73 | f"exit $__rc" |
| 74 | ) |
| 75 | |
| 76 | # ``stdin=DEVNULL`` mirrors the foreground bash path: prevents background |
| 77 | # commands that read fd 0 from blocking on a TTY inherited from clawcodex's |
| 78 | # REPL (see bash_tool.py:_run_bash_with_abort for the same reasoning). |
| 79 | proc = subprocess.Popen( |
| 80 | ["bash", "-lc", wrapped], |
| 81 | cwd=str(cwd), |
| 82 | stdin=subprocess.DEVNULL, |
| 83 | stdout=output_handle, |
| 84 | stderr=subprocess.STDOUT, |
| 85 | start_new_session=True, |
| 86 | ) |
| 87 | |
| 88 | started_at = time.time() |
| 89 | state = LocalShellTaskState( |
| 90 | id=task_id, |
| 91 | type="local_bash", |
| 92 | status="running", |
| 93 | description=description or command, |
| 94 | start_time=started_at, |
| 95 | # ``output_file`` (chapter-10 base field) carries the same string |
| 96 | # as ``output_path`` (bash-specific name kept for legacy readers). |
| 97 | output_file=str(output_path), |
| 98 | command=command, |
| 99 | cwd=str(cwd), |
| 100 | pid=proc.pid, |
| 101 | output_path=str(output_path), |