# ๐Ÿ” Review Report โ€” PR #41 ๋ณ‘ํ•ฉ ์ „ ๋…๋ฆฝ ๋ฆฌ๋ทฐ + ์„ค๊ณ„ ํ•ฉ์˜ (job `ed0e1492`) - **Reviewer**: `planner-reviewer-claude-01` (role: planner,reviewer) ยท 2026-09-26 - **๋Œ€์ƒ**: PR #41 `fix/issue-30-33` **@ `1673818`**, base `main 8ea65bb` (v4.7.4). 6์ปค๋ฐ‹, 30ํŒŒ์ผ `+2047/โˆ’95`, `git diff --check` ํด๋ฆฐ. - **๊ฒ€์ฆ ๊ฒฝ๋กœ**: `/var/folders/.../T/mam-review/loop-20260926T025230Z-a30a6d/1`. **identity ํ™•์ธ**: `HEAD = 1673818bโ€ฆe91f`, `HEAD^{tree} = 00d5e03eโ€ฆ9e92` โ€” ๋ธŒ๋ฆฌํ”„์™€ ์ผ์น˜. - **์•ˆ์ „ ๊ทœ์น™ ์ค€์ˆ˜**: ๊ฒ€์ฆ ๊ฒฝ๋กœยท์†Œ์Šค ์›ŒํฌํŠธ๋ฆฌ(`multi-agent-mux-wt/pr-41`)ยท๋ณธ ์ฒดํฌ์•„์›ƒ(`main 8ea65bb`) ๋ชจ๋‘ ์ˆ˜์ •ยท์ปค๋ฐ‹ยทcheckout ์—†์Œ. ์‹คํ—˜์€ in-process ๋Ÿฌ๋„ˆ(`env -u MAM_VERSION`)ยท๋ž˜ํผ `--dry-run`(`--agent human`, ๋„๋‹ฌ ๋ถˆ๊ฐ€ ๋ธŒ๋กœ์ปค)ยท๋˜๋Œ๋ฆฐ ์‚ฌ๋ณธ์—์„œ๋งŒ. **stall_watch ๋ฅผ ์‹ค์ œ ์„ธ์…˜์— ๋ถ™์ด์ง€ ์•Š์•˜๊ณ **, ์‹ค์ œ herdr ์„œ๋ฒ„์— ์œ„์ž„ ์žก์„ ๋งŒ๋“ค์ง€ ์•Š์•˜๋‹ค. pushยทmergeยทPR ์ƒํƒœ ๋ณ€๊ฒฝ ์—†์Œ. ## ๊ฒฐ๋ก  ์š”์•ฝ **๋ณ‘ํ•ฉ์„ ๋ง‰์„ ๊ฒฐํ•จ์€ ์—†๋‹ค.** #33 ์˜ ์˜ˆ์‚ฐ ์ „๋‹ฌยท์šฐ์„ ์ˆœ์œ„์™€ ๋‘ ์‹œ๊ณ„ ๋ผ๋ฒจ, #30 ๊ฐ์‹œ์ž์˜ "์•Œ๋ฆผ ์ „์šฉ" ์„ฑ์งˆ(๋ฐœํ–‰ํ•˜๋Š” ์ด๋ฒคํŠธ๊ฐ€ `progress` ๋ฟ, ์žก์„ ๋‹ซ๋Š” ์ฝ”๋“œ ๊ฒฝ๋กœ ์—†์Œ)์„ ์ฝ”๋“œยท์‹คํ—˜ยทํ…Œ์ŠคํŠธ๋กœ ํ™•์ธํ–ˆ๋‹ค. ๊ฒฝ๊ณ„ ๊ธฐ์ค€์œผ๋กœ ๋ณด๋ฉด bash ์— ์ƒˆ **ํŒ์ •** ์ด ๋‘ ๊ณณ ๋‚จ์•„ ์žˆ๊ณ (ยง4 ํ‘œ: ๋””์Šคํฌ heartbeat ์œ ์˜ˆ์˜ ์‹œ๊ณ„ ํŒ์ •, ๊ฐ์‹œ ๋Œ€์ƒ ์ œ์™ธ ํŒ์ •) ์ด๋Š” ํ•ญ๋ชฉ 4 ์ด์ „ ๋Œ€์ƒ์ด๋ฏ€๋กœ ๋น„์ฐจ๋‹จ์œผ๋กœ ๋ถ„๋ฅ˜ํ•œ๋‹ค. ๋น„์ฐจ๋‹จ ๊ด€์ฐฐ 7๊ฑด(M1โ€“M7). ์„ค๊ณ„ ํ•ฉ์˜ 4ํ•ญ๋ชฉ์€ ยง8 ์— ๋ณ„๋„๋กœ ๋‹ตํ–ˆ๋‹ค. ## 1. #33 ์˜ˆ์‚ฐ (ํ•ญ๋ชฉ 1) | ํ™•์ธ | ๊ฒฐ๊ณผ | |---|---| | ๋ž˜ํผ ์šฐ์„ ์ˆœ์œ„ | `--timeout` > `MAM_JOB_TIMEOUT_S` > 3600. `--dry-run` ์‹ค์ธก์œผ๋กœ ๋“ฑ๋ก๋œ `timeout_sec`: ์—†์Œโ†’3600, env 900โ†’900, env 900 + `--timeout 120`โ†’**120**, env `abc`โ†’3600 + `WARNING: โ€ฆ ignored (not a positive integer)`, env `-7`โ†’3600 + ๊ฒฝ๊ณ  | | ๋Ÿฌ๋„ˆ ์šฐ์„ ์ˆœ์œ„ | `job_budget`(`runner.py:595-600`): `--job-timeout` > env(์–‘์˜ ์ •์ˆ˜) > 3600. in-process ์‹ค์ธก: ๊ธฐ๋ณธ 3600/๋Œ€๊ธฐ 3900, env 900โ†’900/1200, flag 120 > env 900โ†’120/420, env `abc`ยท`0`โ†’3600(**๋ฌด์–ธ**, M4), `--job-timeout 0/-5/x` โ†’ exit 1 | | ๋ชจ๋“  ์œ„์ž„์— ์ „๋‹ฌ๋˜๋Š”๊ฐ€ | `contract_args()`(`:464-470`)๊ฐ€ **ํ•ญ์ƒ** `--timeout ` ์„ ์•ž์— ๋ถ™์ด๊ณ , ๊ตฌํ˜„ยท๋ฆฌ๋ทฐยท์ˆ˜์ •ยท์žฌ์‹ฌยท์ค‘์žฌยท์ˆœ์‘ ์žก์€ ์ „๋ถ€ `_dispatch โ†’ delegate_job โ†’ _run_cmd(โ€ฆ + contract_args())` ๋ฅผ ์ง€๋‚˜๋ฏ€๋กœ ์˜ˆ์™ธ๊ฐ€ ์—†๋‹ค(์‹ค์ธก `['--timeout','3600']`). ๋ž˜ํผ ์ชฝ์—์„œ๋Š” `--timeout` ํ”Œ๋ž˜๊ทธ๊ฐ€ env ๋ฅผ ์ด๊ธฐ๋ฏ€๋กœ ๋ฃจํ”„์˜ ์˜ˆ์‚ฐ์ด ๋“ฑ๋ก๋œ๋‹ค(B2) | | ๋Œ€๊ธฐ ์ƒํ•œ | `job_wait_s = budget + MAM_WAIT_GRACE_S(300)`(`:603-609`); `wait_for_job` ๊ธฐ๋ณธ `max_wait` ์™€ `MAM_CHILD_WAIT_S` ๊ธฐ๋ณธ๊ฐ’์ด ์ด๋ฅผ ๋”ฐ๋ฅด๊ณ  ๋ช…์‹œ `MAM_CHILD_WAIT_S` ๋Š” ์กด์ค‘(`:729-732`, ๋กœ๊ทธ์— ์ถœ์ฒ˜ ํ‘œ๊ธฐ). `MAM_WAIT_GRACE_S=10` โ†’ 610 ์‹ค์ธก | | ์›๊ฒฉ ์žก | `remote.py` ๋Š” `job.timeout_sec`(๋“ฑ๋ก๋œ ์˜ˆ์‚ฐ)๋กœ ๋ช…๋ น์„ ์ฃฝ์ด๋ฏ€๋กœ ์˜ˆ์‚ฐ์ด ๊ทธ๋Œ€๋กœ ์ ์šฉ๋œ๋‹ค; ๋ผ๋ฒจ์€ ยง2 | ## 2. #33 ๋ผ๋ฒจ (ํ•ญ๋ชฉ 2) - **์ƒ์ˆ˜**: `job_subscriber.py:53-61` `RC_COMPLETED/ERROR/NO_EVENT/BROKER_UNAVAILABLE/WALL_CLOCK = 0/1/2/3/4`, ๋ž˜ํผ `:144` `SUB_RC_*` ๋™์ผ๊ฐ’. `test_issue5_phase1_delegation.py::test_wrapper_exit_code_mapping_literals_are_frozen` ์ด `SUB_RC_*` ์‚ฌ์ „์„ `RC_*` ์™€ **์‚ฌ์ „ ๋‹จ์œ„๋กœ ๋น„๊ต**ํ•˜๊ณ , ๋ž˜ํผ๊ฐ€ ์ˆซ์ž๋กœ ๋น„๊ตํ•˜๋Š” ์ž๋ฆฌ๊ฐ€ ์—†์Œยท๊ตฌ๋…์ž๊ฐ€ ์ˆซ์ž `return` ์„ ํ•˜์ง€ ์•Š์Œ์„ ์ •๊ทœ์‹์œผ๋กœ ๊ณ ์ •ํ•œ๋‹ค. - **๋ž˜ํผ ์ข…๊ฒฐ ๊ฒฝ๋กœ๋ณ„ ๋ผ๋ฒจ**(`finalize_unfinished`, `:860-1000`): | ๊ฒฝ๋กœ | ๋ผ๋ฒจ | |---|---| | ๊ตฌ๋…์ž rc 4 | `clock=wall` โ†’ `close --request wall_clock_timeout` + `error --detail wall_clock_timeout` | | ๊ตฌ๋…์ž rc 2 | `clock=idle` โ†’ `deadline_timeout` | | rc 2 ๋’ค ๋””์Šคํฌ heartbeat ์œ ์˜ˆ๊ฐ€ `$deadline` ์— ๋‹ฟ์Œ | `FINALIZE_CLOCK=wall` โ†’ `wall_clock_timeout`(R3) | | rc 3(broker_unavailable) ์žฌ๊ด€์ธก์ด `$deadline` ์— ๋‹ฟ์Œ | ๊ฐ™์€ ๊ทœ์น™์œผ๋กœ `wall` | | inject ๋‹จ๊ณ„ | `agent_blocked` ์œ ์ง€(`phase != inject` ์กฐ๊ฑด) | | preflight/idle_wait | `cancelled`(`session_busy`/`target_not_found`) ์œ ์ง€ | - ๋“ฑ๋ก๋ถ€ `close --request` ์— `wall_clock_timeout` ์ถ”๊ฐ€(`registry.py:424`), `engine.REQUEST_TO_STATUS["wall_clock_timeout"]="error"`, #34 ํ‘œ์˜ `ORCHESTRATOR_CLOSED` ์— ํฌํ•จ + `.rstrip(":")` ๋กœ ์›๊ฒฉ ๋ฌธ๊ตฌ `wall_clock_timeout: agent command exceededโ€ฆ` ๋„ ์ฒซ ๋‹จ์–ด ํŒ์ •์„ ํ†ต๊ณผ(B5). - `delegate-job wait`: rc 4โ†’2 ๋งคํ•‘(`:1214`)์œผ๋กœ ํ•˜์œ„ ํ˜ธํ™˜(B7, G-8 ์€ idle 30 s/wall 0.5 s ๋กœ ์‹œ๊ณ„๋ฅผ ๋ถ„๋ฆฌํ•ด ๊ฒฐ์ •์ ). - ๋ฒฝ์‹œ๊ณ„๋กœ ๋‹ซํžŒ ๋’ค ์—์ด์ „ํŠธ์˜ ๋Šฆ์€ `completed`: `finalize_unfinished` ๊ฐ€ ๋””์Šคํฌ ์ƒํƒœ๊ฐ€ ์ด๋ฏธ ์ข…๋‹จ์ด๋ฉด ๊ฑด๋„ˆ๋›ฐ๊ณ (`:875-877`), `engine.transition` ์ด ์ข…๋‹จ ๋’ค ์ข…๋‹จ์„ ๊ฑฐ๋ถ€ํ•˜๋Š” ๊ธฐ์กด ๊ทœ์น™(#23 ๋ฆฌ๋ทฐ์—์„œ ํ™•์ธํ•œ "conflicting โ€ฆ after terminal")์ด ๊ทธ๋Œ€๋กœ๋‹ค โ€” ์ด PR ์ด ๋ฐ”๊พธ์ง€ ์•Š์•˜๋‹ค. ## 3. #30 ๊ฐ์‹œ์ž `stall_watch.py` (ํ•ญ๋ชฉ 3) - **์žก์„ ๋‹ซ๋Š” ๊ฒฝ๋กœ ์—†์Œ**: ํŒŒ์ผ ์ „์ฒด์—์„œ `--event` ๋Š” `_notify` ์˜ `progress` ํ•˜๋‚˜๋ฟ(`:237`), `close`/`error`/`cancelled`/`update_status`/`transition` ํ˜ธ์ถœ 0๊ฑด(grep). `judge()` ๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜๋กœ `NOTIFY_BLOCKED`/`NOTIFY_IDLE_AND_NUDGE`/`None` ๋งŒ ๋‚ธ๋‹ค. S12 ๊ฐ€ ์†Œ์Šค ์ˆ˜์ค€์—์„œ ๊ฐ™์€ ๊ฒƒ์„ ๊ณ ์ •ํ•œ๋‹ค. ์ข…๋ฃŒ ์ฝ”๋“œ๋Š” 0(์žก ์ข…๋‹จ)ยท2(herdr ๋ถˆ๊ฐ€)๋ฟ โ†’ **๊ฐฑ์‹  1 ์ค€์ˆ˜**. - **herdr ์กฐํšŒ ๋ฐฉ์‹**: `watcher.wait_once`(`agent wait --until โ€ฆ --timeout`)์˜ ์„œ๋ฒ„ ์†Œ์œ  ๋ฐฐ๋ฆฌ์–ด๋ฅผ ์ƒํƒœ๋ณ„ `until` ๋กœ ๋Œ๋ฆฌ๊ณ (`:274-284`), idle ๋’ค์—๋Š” 30 s ๋‹จ์œ„๋กœ ์žฌํŒ์ •ํ•œ๋‹ค. ํด๋งยท๊ตฌ๋…์ด ์•„๋‹ˆ๋ผ ๋ฐฐ๋ฆฌ์–ด + ํŒ์ • ์‹œ์ ์˜ `agent get`/`pane process-info`/`agent read`. - **blocked**: `pane read --source detection --lines 8` ์˜ ์ฒซ ์ค„(๋น„ASCII ์ œ๊ฑฐ, 160์ž)์„ ๋ถ™์—ฌ `stalled:blocked <๋ฌธ๊ตฌ>` ๋ฅผ ์—ํ”ผ์†Œ๋“œ๋‹น 1ํšŒ ๋ฐœํ–‰(`:318-324`), **ํ‚ค ์ž…๋ ฅ ์—†์Œ** โ€” ๊ฐฑ์‹  1 ์˜ "์ž๋™ ์ˆ˜๋ฝ ์—†์Œ" ๊ณผ ์ผ์น˜. `blocked โ†’ ๋‹ค๋ฅธ ์ƒํƒœ โ†’ blocked` ๋ฉด ๋‹ค์‹œ 1ํšŒ. - **idle**: `MAM_STALL_IDLE_S`(600) ์—ฐ์† idle + `agent_pid`(foreground ํ”„๋กœ์„ธ์Šค, shell ์•„๋‹˜) ์žˆ์Œ + ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž์† ์—†์Œ(herdr pid โ†’ OS ์ž์† ํŠธ๋ฆฌ์—์„œ ์–ด๋Œ‘ํ„ฐ `resident_children` ๊ณผ heartbeat ํƒ€์ด๋จธ ์ œ์™ธ, `:118-136`) + ์ž…๋ ฅ์ฐฝ ๋น„์–ด ์žˆ์Œ(`extract_input_region == ""`) ์ผ ๋•Œ๋งŒ `stalled:idle` + ์žฌ์ฃผ์ž… **์žก๋‹น 1ํšŒ**(`st.nudged`). `state_change_seq` ๊ฐ€ ์ฐฝ ์•ˆ์—์„œ ๋ฐ”๋€Œ์—ˆ์œผ๋ฉด ์ฐฝ์„ ๋‹ค์‹œ ์‹œ์ž‘(R4). ์ž…๋ ฅ ์‚ฌ์‹ค์ด ์—†๋Š” ์—์ด์ „ํŠธ๋Š” ๋ณด๋ฅ˜ + 1ํšŒ ์•Œ๋ฆผ. ์žฌ์ฃผ์ž… ๋ฌธ๊ตฌ(`NUDGE`)๋Š” ์˜๋ฌธยทASCIIยท1๋ฌธ์žฅ์ด๋ฉฐ "๋๋‚ฌ์œผ๋ฉด completed/error ๋ฐœํ–‰, ์•„๋‹ˆ๋ฉด ์ด์–ด์„œ" ๋ผ ์˜ค์ž‘๋™ ์—ฌ์ง€๊ฐ€ ์ž‘๋‹ค. - **์›๊ฒฉยทhuman ์ œ์™ธ**: ๋ž˜ํผ `start_stall_watch`(`:684-702`)๊ฐ€ `remote:`/`human` ์ด๋ฉด ๊ธฐ๋™ํ•˜์ง€ ์•Š๊ณ , `MAM_STALL_WATCH=0` ์œผ๋กœ ๋Œ ์ˆ˜ ์žˆ๋‹ค(S9/S10). - **herdr ์‹คํŒจ**: `TRANSPORT_ERROR`/`AGENT_NOT_FOUND` ๋Š” "์ด๋ฒคํŠธ ์—†์Œ"์œผ๋กœ ์ฝ์ง€ ์•Š๊ณ  1ํšŒ ์•Œ๋ฆผ + ๋ฐฑ์˜คํ”„, `MAM_STALL_HERDR_FAIL_S`(120) ๋’ค rc 2(S8). - **์ˆ˜๋ช…**: ๋ž˜ํผ๊ฐ€ `wait "$sub_pid"` ์งํ›„ `stop_stall_watch`(TERM โ†’ `os._exit(0)`)๋กœ ๊ฑฐ๋‘”๋‹ค(`:385`, `:560`). **M3**: `start_stall_watch` ์™€ `stop_stall_watch` ์‚ฌ์ด์˜ ๋น„์ •์ƒ ์ข…๋ฃŒ(`set -e` ์˜ค๋ฅ˜, ๊ฒ€์ฆ ํ›… ์‹คํŒจ์˜ `exit`, ๋ž˜ํผ SIGKILL)๋ฅผ ๋ฎ๋Š” EXIT trap ์ด ์—†๋‹ค. ๊ฐ์‹œ์ž๋Š” ์žก ๋ ˆ์ฝ”๋“œ๊ฐ€ ์ข…๋‹จ์ด ๋˜๋ฉด ๋‹ค์Œ ๋ฐ˜๋ณต(โ‰ค 60 s ๋ฐฐ๋ฆฌ์–ด)์—์„œ ์Šค์Šค๋กœ ๋๋‚˜๋ฏ€๋กœ ์žก์ด ๋‹ซํžˆ๋Š” ํ•œ ๊ณ ์•„ ์ˆ˜๋ช…์€ ์œ ๊ณ„์ด์ง€๋งŒ, ์žก์ด `running` ์œผ๋กœ ๋‚จ๋Š” ๊ฒฝ์šฐ(๋ž˜ํผ -9)์—๋Š” herdr ๊ฐ€ ์‚ด์•„ ์žˆ๋Š” ๋™์•ˆ ๊ณ„์† ๋ˆ๋‹ค. `.stall.out` ์€ `.mam/jobs/.stall.out` ์— ๋‚จ๋Š”๋‹ค(์ž‘๊ณ , #20 ๊ฐ€๋“œ์˜ `*.json` ๊ฐ์‹œ ๋ฐ–). - **์‹ค์ œ ์‹œํ—˜ A/B ์žฌํ˜„์„ฑ**: ์‹ค์ œ ์„ธ์…˜์— ๋ถ™์ด๋Š” ๊ฒƒ์€ ๊ธˆ์ง€๋ผ ์žฌํ˜„ํ•˜์ง€ ์•Š์•˜๋‹ค. mock herdr ์œ„์˜ S2/S3/S7/R1/R2/R4 ๊ฐ€ ๊ฐ™์€ ์‹ ํ˜ธ ํ๋ฆ„์„ ์‹ค์ œ `python -m lib_py.stall_watch` ํ”„๋กœ์„ธ์Šค๋กœ ๋Œ๋ฆฐ๋‹ค. ## 4. ๊ฒฝ๊ณ„ ๊ธฐ์ค€ โ€” bash ์— ๋“ค์–ด์˜จ ํŒ์ • (ํ•ญ๋ชฉ 4) | ์œ„์น˜ | ๋‚ด์šฉ | ์„ฑ๊ฒฉ | ๋ถ„๋ฅ˜ | |---|---|---|---| | ๋ž˜ํผ `:146-151` | `MAM_JOB_TIMEOUT_S` ์–‘์˜ ์ •์ˆ˜ ๊ฒ€์ฆยท๊ฒฝ๊ณ  | ์ž…๋ ฅ ๊ฒ€์ฆ(๊ธฐ๋ณธ๊ฐ’ ๊ฒฐ์ •) | ํ•ญ๋ชฉ 4 ์ด์ „ ๋Œ€์ƒ, ๋น„์ฐจ๋‹จ | | `finalize_unfinished` `:902`, `:950` | ๋””์Šคํฌ heartbeat ์œ ์˜ˆ/broker ์žฌ๊ด€์ธก ๋ฃจํ”„๊ฐ€ `$deadline` ์— ๋‹ฟ์•˜๋Š”์ง€๋กœ `FINALIZE_CLOCK=wall` | **์‹œ๊ณ„ ํŒ์ •** โ€” Python ๋ฐ˜ํ™˜๊ฐ’์ด ์•„๋‹ˆ๋ผ bash ๊ฐ€ ๊ฒฐ์ • | ํ•ญ๋ชฉ 4 ์ด์ „ ๋Œ€์ƒ, ๋น„์ฐจ๋‹จ(**M1**) | | `:956-959` | `sub_rc == SUB_RC_WALL_CLOCK โ†’ clock=wall`, `phase != inject` ๋ฉด `wall_clock_timeout` | rcโ†’๋ผ๋ฒจ **์šด๋ฐ˜** + inject ์˜ˆ์™ธ(ํŒ์ •) | ์šด๋ฐ˜์€ ๊ธฐ์ค€ ๋ถ€ํ•ฉ; inject ์˜ˆ์™ธ๋Š” ๊ธฐ์กด ๊ตฌ์กฐ | | `:684-702` | remote/human ์ œ์™ธ, `MAM_STALL_WATCH` | ๊ฐ์‹œ ์—ฌ๋ถ€ **ํŒ์ •** | ํ•ญ๋ชฉ 4 ์ด์ „ ๋Œ€์ƒ, ๋น„์ฐจ๋‹จ(**M2**) | | `cmd_wait` `:1214` | rc 4โ†’2 | ํ˜ธํ™˜ ๋งคํ•‘(์šด๋ฐ˜) | ๋ถ€ํ•ฉ | | `stop_stall_watch` | rcโ‰ 0 ์ด๋ฉด ๋กœ๊ทธ ๊ผฌ๋ฆฌ ์ถœ๋ ฅ | ์‹คํ–‰๋ถ€ | ๋ถ€ํ•ฉ | "bash ๋Š” ๋ผ๋ฒจ์„ ๋‚˜๋ฅด๊ธฐ๋งŒ ํ•œ๋‹ค"๋Š” ์ฃผ์žฅ์€ **๊ตฌ๋…์ž rc ๊ฒฝ๋กœ์— ํ•œํ•ด** ๋งž๋‹ค. ์œ ์˜ˆ ๋ฃจํ”„์˜ ์‹œ๊ณ„ ํŒ์ •(M1)๊ณผ ๊ฐ์‹œ ๋Œ€์ƒ ํŒ์ •(M2)์€ bash ์˜ ๊ฒฐ์ •์ด๋‹ค. ๋‘˜ ๋‹ค ํ•ญ๋ชฉ 4(๋ž˜ํผ ์ข…๊ฒฐ ์ฒ˜๋ฆฌ์˜ Python ์ด์ „)์—์„œ ์˜ฎ๊ธธ ๋Œ€์ƒ์ด๊ณ  PR ๋ณธ๋ฌธ์ด ํ•ญ๋ชฉ 4 ๋ฅผ ๋ฏธ๋ฃฌ๋‹ค๊ณ  ๋ฐํ˜”์œผ๋ฏ€๋กœ ๋น„์ฐจ๋‹จ. ## 5. ๊ธฐ์กด ํ…Œ์ŠคํŠธ ์กฐ์ •ยท์ƒˆ ํ…Œ์ŠคํŠธ (ํ•ญ๋ชฉ 5) - G-8: idle 0.5/wall 0.5 ์˜ ๊ฒฝํ•ฉ์„ idle 30/wall 0.5 ๋กœ ํ’€์–ด **์–ด๋А ์‹œ๊ณ„์ธ์ง€๊ฐ€ ๊ฒฐ์ •์ **์ด ๋๊ณ  rc ๋Š” `RC_WALL_CLOCK` ์œผ๋กœ ๋‹จ์–ธ โ€” ์˜๋„ ์œ ์ง€. - issue5 phase1: ์Šค๋ƒ…์ƒท์ด "์ˆซ์ž return ๊ธˆ์ง€ยท์ด๋ฆ„ ๋น„๊ต๋งŒ" ์œผ๋กœ **๊ฐ•ํ™”**๋๊ณ  `help-registry.txt` ๋Š” `close` ์„ ํƒ์ง€ ๊ฐฑ์‹ . issue18: `contract_args` ์— `--timeout 3600` ์ด ์•ž์— ์˜ค๋Š” ๊ฒƒ๋งŒ ๊ฐฑ์‹ . `conftest.py`: mock herdr ์— `force_error_code`ยทdetection ์†Œ์Šคยท์‹ค์ œ `process-info` ํ˜•ํƒœยท`state_change_seq`ยท`wait_blocks_s` ์ถ”๊ฐ€ โ€” ๊ธฐ์กด ๋™์ž‘์€ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์œ ์ง€. - ์ƒˆ ํ…Œ์ŠคํŠธ: `_env()` ๊ฐ€ `HOME`ยท`WORKSPACE_ROOT`ยท๋กœ๊ทธ ๋ฃจํŠธ๋ฅผ `tmp_path` ๋กœ, ๋ธŒ๋กœ์ปค๋ฅผ ๋„๋‹ฌ ๋ถˆ๊ฐ€๋กœ ๊ณ ์ •; ๊ฐ์‹œ์ž๋Š” ์‹ค์ œ `python -m lib_py.stall_watch` ํ”„๋กœ์„ธ์Šค, ๋ž˜ํผ๋„ ์‹ค์ œ ์Šคํฌ๋ฆฝํŠธ(`--agent human`), herdr ๋Š” mock. ์‹ค์ œ `.mam`ยทํ™ˆยทherdr ์ ‘์ด‰ ์—†์Œ(ยง6 ์‹ค์ธก). - pane ํ”ฝ์Šค์ฒ˜ 5์ข…: `extract_input_region` ์„ 5๊ฐœ idle ์บก์ฒ˜์— ๋Œ๋ ค **์ „๋ถ€ `''`**(S14 ์˜ typing ๋ณ€ํ˜•์ด ๋น„์–ด ์žˆ์ง€ ์•Š์Œ์„ ๊ณ ์ •). ์บก์ฒ˜ ์ž์ฒด๊ฐ€ ์‹ค์ œ ํ™”๋ฉด๊ณผ ๊ฐ™์€์ง€๋Š” ์ด ๋ฆฌ๋ทฐ์—์„œ ์žฌํ˜„ํ•  ์ˆ˜ ์—†๊ณ (์‹ค์ œ ์„ธ์…˜ ์ ‘๊ทผ ๊ธˆ์ง€), PR ์˜ "herdr 0.9.1 ์บก์ฒ˜" ๊ธฐ๋ก์— ์˜์กดํ•œ๋‹ค. ## 7. ๋ฌธ์„œ (ํ•ญ๋ชฉ 6) - delegate-job SKILL.md: `MAM_JOB_TIMEOUT_S`ยท`MAM_STALL_IDLE_S`/`MAM_STALL_HERDR_FAIL_S`/`MAM_STALL_WATCH=0` ํ–‰(`:49-50`), ์ข…๋ฃŒ ์ฝ”๋“œ ํ‘œ์— `2 | Wall clock | error (wall_clock_timeout)` ํ–‰(`:100`) ์ถ”๊ฐ€ โ€” ์ฝ”๋“œ์™€ ์ผ์น˜. **M5**: ๊ทธ ํ–‰์˜ "was `deadline_timeout` before v4.7.5" ๋Š” ๋ธŒ๋ฆฌํ”„์˜ ๋ณ‘ํ•ฉ ๋ฒ„์ „(**4.8.0**, MINOR)๊ณผ ์–ด๊ธ‹๋‚œ๋‹ค โ€” ๋ฆด๋ฆฌ์Šค ์ปค๋ฐ‹์—์„œ ๋ฌธ๊ตฌ ์ˆ˜์ •. - loop SKILL.md: `--job-timeout` ํ–‰(`:209`)๊ณผ "ํ•˜์œ„ ์œ„์ž„ ์žก" ์ ˆ์˜ `MAM_CHILD_WAIT_S` ๊ธฐ๋ณธ๊ฐ’์ด "์žก ์˜ˆ์‚ฐ + 300 s" ๋กœ ๊ฐฑ์‹ (`:222`) โ€” ์ฝ”๋“œ์™€ ์ผ์น˜. **M6**: `MAM_WAIT_GRACE_S`(๋Ÿฌ๋„ˆ์˜ +300 ์„ ๋ฐ”๊พธ๋Š” ์†์žก์ด, `runner.py:608`)๋Š” ์–ด๋А ๋ฌธ์„œ์—๋„ ์—†๋‹ค. - ๋ž˜ํผ usage ์˜ `--timeout` ๊ธฐ๋ณธ๊ฐ’ ์„œ์ˆ  ๊ฐฑ์‹  โœ“. ## 8. ์„ค๊ณ„ ํ•ฉ์˜ **(A) #30 ์•Œ๋ฆผ ์ „์šฉ ์›์น™ โ€” ๋™์˜.** ๊ทผ๊ฑฐ: heartbeat ๋Š” ํ”„๋กœ์„ธ์Šค ์ƒ์กด๋งŒ ์ฆ๋ช…ํ•˜๊ณ (#18 ์˜ ์ ˆ์ถฉ), herdr ์˜ `agent_status` ๋„ "working ์œผ๋กœ ๋ณด๊ณ ๋˜๋Š” ๋ฉˆ์ถค"(codex ์žฌ์ ‘์† ์‚ฌ๋ก€)์„ ๋ชป ์žก๋Š”๋‹ค. ๊ทธ๋Ÿฐ ๋ถˆ์™„์ „ํ•œ ์‹ ํ˜ธ๋กœ ์žก์„ ๋‹ซ์œผ๋ฉด #18 ์ด ์„ธ์šด "ํ”„๋กœํ† ์ฝœ ์ด๋ฒคํŠธ๋งŒ์ด ์ข…๊ฒฐ ๊ทผ๊ฑฐ" ๊ณ„์•ฝ์„ ๊นจ๊ณ , ์ •์ƒ์ ์ธ ์žฅ๊ธฐ ์ž‘์—…(์ „์ฒด ์Šค์œ„ํŠธ 2ํšŒ)๊นŒ์ง€ ๋‹ซ๊ฒŒ ๋œ๋‹ค. ์•Œ๋ฆผ์€ ์‚ฌ๋žŒยท๋ฃจํ”„๊ฐ€ ํŒ๋‹จํ•  ๊ทผ๊ฑฐ๋ฅผ ์ฃผ๋ฉด์„œ ๊ณ„์•ฝ์„ ์œ ์ง€ํ•œ๋‹ค. **์ด PR ์€ ๊ทธ๊ฒƒ์„ ์ฝ”๋“œ๋กœ ๊ฐ•์ œํ•œ๋‹ค**: `stall_watch.py` ์˜ ์œ ์ผํ•œ ๋ฐœํ–‰๊ธฐ `_notify` ๊ฐ€ `progress` ๋งŒ ๋‚ด๊ณ , ์ข…๊ฒฐ API(`close`/`error`/`cancelled`/`transition`) ํ˜ธ์ถœ์ด ํŒŒ์ผ์— ์—†์œผ๋ฉฐ, `judge()` ์˜ ๋ฐ˜ํ™˜๊ฐ’์ด ์•Œ๋ฆผ ๋‘ ์ข…๋ฅ˜๋ฟ์ด๋‹ค. S12 ๊ฐ€ ์†Œ์Šค ์ˆ˜์ค€์—์„œ ์ด๋ฅผ ๊ณ ์ •ํ•˜๋ฏ€๋กœ ํ›„์† ์ปค๋ฐ‹์ด ์กฐ์šฉํžˆ ์ข…๊ฒฐ ๊ฒฝ๋กœ๋ฅผ ๋„ฃ๊ธฐ ์–ด๋ ต๋‹ค. ํ•œ ๊ฐ€์ง€ ๋ณด๊ฐ• ๊ถŒ๊ณ : `judge()` ์˜ ๋ฐ˜ํ™˜ ํƒ€์ž…์„ ์—ด๊ฑฐํ˜•์œผ๋กœ ์ขํ˜€ "๋‹ซ๊ธฐ" ๋ผ๋Š” ๊ฐ’์ด ์• ์ดˆ์— ํ‘œํ˜„ ๋ถˆ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋‘๋ฉด ํ…Œ์ŠคํŠธ ์—†์ด๋„ ๊ตฌ์กฐ๊ฐ€ ์›์น™์„ ๋งํ•œ๋‹ค. **(B) ์˜ˆ์‚ฐ ์—ฐ์žฅ์„ #39 ๋กœ ๋ถ„๋ฆฌ โ€” ์กฐ๊ฑด๋ถ€ ๋™์˜.** ๋Ÿฌ๋„ˆ๊ฐ€ ๋ž˜ํผ๋ฅผ ๋™๊ธฐ ํ˜ธ์ถœํ•˜๋Š” ๊ตฌ์กฐ์—์„œ "๋ž˜ํผ๊ฐ€ ๋ฉˆ์ถฐ์„œ ๋ฌป๊ณ  ๋ฃจํ”„๊ฐ€ ์ •์ฑ…์œผ๋กœ ๋‹ตํ•œ๋‹ค" ๋Š” ์›์•ˆ์€ ๋ž˜ํผ ์•ˆ์—์„œ ์ •์ฑ…์„ ๊ฒฐ์ •ํ•˜๊ฑฐ๋‚˜ ํŒŒ์ผ ๊ธฐ๋ฐ˜ ๊ฒฐ์ • ์ฑ„๋„์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•˜๊ณ , `state_change_seq` ์ •์ฒด๋Š” ์ •์ƒ ์žฅ๊ธฐ ์ž‘์—…๋„ ๊ฑฐ๋ถ€ํ•œ๋‹ค๋Š” PR ์˜ ๊ด€์ฐฐ์— ๋™์˜ํ•œ๋‹ค โ€” ์ด PR ์˜ ๋ฒ”์œ„์—์„œ ๋ถ„๋ฆฌํ•œ ๊ฒƒ์€ ๋งž๋‹ค. #39 ๋ฐฉ์‹(์—์ด์ „ํŠธ๊ฐ€ ํ”„๋กœํ† ์ฝœ ์ด๋ฒคํŠธ๋กœ ์—ฐ์žฅ์„ ์š”์ฒญ)์˜ ์œ„ํ—˜: (1) ์—์ด์ „ํŠธ๊ฐ€ ๋ฐ˜๋ณต ์š”์ฒญํ•ด ์‚ฌ์‹ค์ƒ ๋ฌดํ•œ ์˜ˆ์‚ฐ์ด ๋˜๋Š” ๊ฒƒ, (2) ์›๊ฒฉ ์žก์€ ๋Ÿฌ๋„ˆ ์ชฝ `timeout_sec` ์ด ๋ณ„๋„ ์‹œ๊ณ„๋ผ ์š”์ฒญ์ด ๋‹ฟ์•„๋„ ๋ช…๋ น์ด ๋จผ์ € ์ฃฝ์„ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ, (3) ์ƒํ•œ์ด ์ •์ฑ…์ด ์•„๋‹ˆ๋ผ ์—์ด์ „ํŠธ์˜ ์ž๊ธฐ ๋ณด๊ณ ์— ๊ฑธ๋ฆฌ๋Š” ๊ฒƒ. **๋ณด์™„์ฑ… ํ•œ ๊ฐ€์ง€**: ์—ฐ์žฅ ์š”์ฒญ์„ `progress --detail extend_request:` ๋กœ ๋ฐ›๋˜, **๊ฒฐ์ •์€ ๋ฐ›๋Š” ์ชฝ(๊ตฌ๋…์ž/๋ฃจํ”„)์ด `MAM_JOB_EXTEND_MAX`(๊ธฐ๋ณธ 1)ยท์—ฐ์žฅ ์ด๋Ÿ‰ ์ƒํ•œยท์ง์ „ ์—ฐ์žฅ ๋’ค `state_change_seq` ๋ณ€ํ™” ์—ฌ๋ถ€**๋กœ ๋‚ด๋ฆฌ๊ณ , ์Šน์ธ ์‹œ ๋“ฑ๋ก๋ถ€์˜ `timeout_sec` ์„ CAS ๋กœ ๊ฐฑ์‹ ํ•ด ์›๊ฒฉ ๋Ÿฌ๋„ˆ๋„ ๊ฐ™์€ ๊ฐ’์„ ์ฝ๊ฒŒ ํ•œ๋‹ค โ€” ์ฆ‰ ์š”์ฒญ์€ ์—์ด์ „ํŠธ๊ฐ€, ๊ฒฐ์ •์€ Python ์ด(๊ฒฝ๊ณ„ ๊ธฐ์ค€). ์ƒํ•œ์€ ๋‚จ๊ธด๋‹ค. **(C) #40 ํŠธ๋ฆฌ๊ฑฐ โ€” ๋™์˜(๋ž˜ํผ ๊ตฌ๋…์ž ์ชฝ).** ์ œ์–ด ํ‰๋ฉด(YAMLยทSQLite ์„ธ์…˜ ๋“ฑ๋ก๋ถ€)์€ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ ์†Œ์œ (#15/#20/#21)์ด๋ฏ€๋กœ ์—์ด์ „ํŠธ ํ”„๋กœ์„ธ์Šค๊ฐ€ ์‹คํ–‰ํ•˜๋Š” `publish_event.py` ์—์„œ ์„ธ์…˜ ID ๋ฅผ ์บก์ฒ˜ยท๊ณ ์ •ํ•˜๋Š” ๊ฒƒ์€ ์†Œ์œ ๊ถŒ์„ ๊ฑฐ์Šค๋ฅธ๋‹ค. ๋‘ ํ›„๋ณด ์ค‘ **๋ž˜ํผ ๊ตฌ๋…์ž**๊ฐ€ ๋งž๋‹ค: ๋ฃจํ”„ ๋Ÿฌ๋„ˆ์˜ `wait_for_job` ์€ ๋ฃจํ”„ ๋ฐ– ์ง์ ‘ ์œ„์ž„(`delegate-job submit --type direct`, ์›ŒํฌํŠธ๋ฆฌ creator ์˜ ์‹ค์ œ ์‹œ๋‚˜๋ฆฌ์˜ค)์„ ์ง€๋‚˜์ง€ ์•Š์ง€๋งŒ, ๋ž˜ํผ ๊ตฌ๋…์ž๋Š” ๋ชจ๋“  ์œ„์ž„(๋ฃจํ”„ ์•ˆํŒŽ, `MAM_NO_SUBSCRIBER_DAEMON=1` ์ธ ์›ŒํฌํŠธ๋ฆฌ creator ํฌํ•จ โ€” ๊ทธ ๊ฒฝ์šฐ ๋ž˜ํผ๊ฐ€ ์žก๋ณ„ ๊ตฌ๋…์ž๋ฅผ ์ง์ ‘ ๋„์šด๋‹ค)์˜ `completed` ๋ฅผ ๋ฐ›๋Š” ๋‹จ์ผ ์ง€์ ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๋ฃจํ”„ ๋Ÿฌ๋„ˆ๋Š” ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ์ฝ๊ธฐ๋งŒ ํ•˜๋ฉด ๋œ๋‹ค. **์ˆœ์„œ**: ํ•ญ๋ชฉ 4 ๊ฐ€ ๋ž˜ํผ ์ข…๊ฒฐ ์ฒ˜๋ฆฌ๋ฅผ Python ์œผ๋กœ ์˜ฎ๊ธธ ๋•Œ ๊ทธ ์ข…๊ฒฐ ํ•ธ๋“ค๋Ÿฌ์— `capture_active_session_id โ†’ auto_pin_if_needed` ๋ฅผ ๋ผ์›Œ ๋„ฃ๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์‹ธ๋‹ค โ€” ํ•ญ๋ชฉ 4 ์— ํก์ˆ˜. ํ•ญ๋ชฉ 4 ๋ณด๋‹ค ๋จผ์ € ํ•ด์•ผ ํ•œ๋‹ค๋ฉด ๊ตฌ๋…์ž์˜ `completed` ์ฒ˜๋ฆฌ์— Python ํ›…์œผ๋กœ ๋„ฃ๊ณ  bash ๋Š” ์†๋Œ€์ง€ ์•Š๋Š”๋‹ค. **(D) ํ•ญ๋ชฉ 4 ๋ฅผ ๋ฏธ๋ฃฌ ๊ฒƒ โ€” ์กฐ๊ฑด๋ถ€ ๋™์˜.** ์ด PR ์ด ํ•ญ๋ชฉ 4 ๊นŒ์ง€ ์•ˆ์•˜๋‹ค๋ฉด ๋ผ๋ฒจ ๋ถ„๋ฆฌยท๊ฐ์‹œ์žยท์ข…๊ฒฐ ์ฒ˜๋ฆฌ ์ด์ „์ด ํ•œ ๋ฆฌ๋ทฐ ์™•๋ณต์— ๊ฒน์ณ ๊ฒ€์ฆ ๋ฉด์ ์ด ๋‘ ๋ฐฐ๊ฐ€ ๋์„ ๊ฒƒ์ด๋ฏ€๋กœ, bash ๋ณ€๊ฒฝ์„ ๋ผ๋ฒจ ์šด๋ฐ˜์œผ๋กœ ์ตœ์†Œํ™”ํ•œ ์„ ํƒ์€ ํƒ€๋‹นํ•˜๋‹ค. ์กฐ๊ฑด์€ ยง4 ํ‘œ์˜ ๋‘ ํŒ์ •(M1ยทM2)์ด **ํ•ญ๋ชฉ 4 ์˜ ๋ช…์‹œ์  ์ด์ „ ๋ชฉ๋ก**์— ์˜ฌ๋ผ๊ฐ€๋Š” ๊ฒƒ์ด๋‹ค. ํ•ญ๋ชฉ 4 ์—์„œ Python ์œผ๋กœ ์˜ฎ๊ฒจ์•ผ ํ•  ๊ฒƒ: (1) `finalize_unfinished` ์ „์ฒด โ€” ํŠนํžˆ ๋””์Šคํฌ heartbeat ์œ ์˜ˆ ๋ฃจํ”„์™€ `$deadline` ๋น„๊ต๋กœ ์‹œ๊ณ„๋ฅผ ์ •ํ•˜๋Š” `FINALIZE_CLOCK`(`:902`, `:950`), `sub_rc โ†’ close_req` ๋งคํ•‘๊ณผ inject ์˜ˆ์™ธ(`:956-959`); ๊ตฌ๋…์ž๊ฐ€ "์–ด๋А ์‹œ๊ณ„์ธ์ง€" ๋ฅผ ์ด๋ฏธ rc ๋กœ ์•Œ๊ณ  ์žˆ์œผ๋ฏ€๋กœ ์œ ์˜ˆ ๋ฃจํ”„ ์ž์ฒด๋ฅผ ๊ตฌ๋…์ž(๋˜๋Š” ๊ทธ ์˜†์˜ Python ์ข…๊ฒฐ๊ธฐ)๋กœ ์˜ฎ๊ธฐ๋ฉด bash ์˜ ํŒ์ •์ด ์‚ฌ๋ผ์ง„๋‹ค. (2) `start_stall_watch` ์˜ ๊ฐ์‹œ ์—ฌ๋ถ€ ํŒ์ •(remote/human/`MAM_STALL_WATCH`) โ€” Python ์ข…๊ฒฐ๊ธฐ/๊ตฌ๋…์ž๊ฐ€ ์žก ๋ ˆ์ฝ”๋“œ์˜ `transport`ยท`agent` ๋กœ ๊ฒฐ์ •ํ•˜๊ณ  bash ๋Š” ํ”„๋กœ์„ธ์Šค๋งŒ ๋„์šด๋‹ค. (3) `MAM_JOB_TIMEOUT_S` ๊ฒ€์ฆ โ€” ๋“ฑ๋ก๋ถ€ `register` ๊ฐ€ ์˜ˆ์‚ฐ ๊ธฐ๋ณธ๊ฐ’์„ ๊ฒฐ์ •ํ•˜๊ฒŒ ํ•˜๋ฉด ๋ž˜ํผยท๋Ÿฌ๋„ˆ์˜ ์ด์ค‘ ๊ฒ€์ฆ(M4 ์˜ ๋ถˆ์ผ์น˜)๋„ ์‚ฌ๋ผ์ง„๋‹ค. bash ์— ๋‚จ๊ธธ ๊ฒƒ: ๊ฐ์‹œ์žยท๊ตฌ๋…์ž ํ”„๋กœ์„ธ์Šค ๊ธฐ๋™/ํšŒ์ˆ˜(`stop_stall_watch`), ํ‚ค ์ฃผ์ž…, ๋กœ๊ทธ ๊ผฌ๋ฆฌ ์ถœ๋ ฅ. ## 6. ํ…Œ์ŠคํŠธ ์‹คํ–‰๊ณผ **์ฐจ๋‹จ ๊ฒฐํ•จ N1** (ํ•ญ๋ชฉ 5ยท7) ### 6-1. ๊ฒ€์ฆ ๊ฒฝ๋กœ์—์„œ์˜ ์‹คํ–‰ ``` tests/test_issue30_stall_watch.py tests/test_issue33_budget.py tests/test_tier1_unit.py tests/test_issue5_phase1_delegation.py tests/test_issue18_liveness_contract.py tests/test_issue34_job_tree.py tests/test_issue14_delivery_ack.py tests/test_loop_cli.py โ†’ 3 failed, 222 passed, 48 skipped in 679.80s (0:11:19) FAILED test_issue33_budget.py::test_b3_a_heartbeating_job_that_spends_its_budget_is_wall_clock_timeout FAILED test_issue18_liveness_contract.py::test_send_keys_safe_is_confirmed_by_the_job_started_event FAILED test_issue14_delivery_ack.py::test_wrapper_honours_registry_ack_over_unconfirmed_inject (TimeoutExpired 90 s) ``` - **skip 48 = `test_issue30_stall_watch.py` ์ „์ฒด.** `:39` ์˜ ๋ชจ๋“ˆ ์ˆ˜์ค€ `pytestmark = skipif(not linux, "real process trees via /proc")` ๊ฐ€ ์ˆœ์ˆ˜ ํ•จ์ˆ˜ ์‹œํ—˜(judge ํ‘œยทS12ยทS14ยท`scoped_env`ยท์ถ”์ถœ๊ธฐ R1/R1b)๊ณผ mock herdr ๋งŒ ์“ฐ๋Š” S9/S10/S11 ๊นŒ์ง€ ๊ฑด๋„ˆ๋›ด๋‹ค. ์‚ฌ๋ณธ์—์„œ ๊ทธ ๋งˆํฌ๋งŒ ์ง€์šฐ๊ณ  `/proc` ๋ฌด๊ด€ ์‹œํ—˜์„ ๋Œ๋ฆฌ์ž **29 passed** โ€” ์ด ํ˜ธ์ŠคํŠธ์—์„œ ๋Œ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ๋“ค์ด๋‹ค(M8). `background_children` ์˜ macOS `ps` ๊ฒฝ๋กœ๋Š” ์Šค์œ„ํŠธ๊ฐ€ ์•ˆ ํƒ€๋ฏ€๋กœ ์ง์ ‘ ํ™•์ธํ–ˆ๋‹ค: ์ž์‹ `sleep` ๊ฐ์ง€, heartbeat ํƒ€์ด๋จธ ์ œ์™ธ, `resident` ํ•„ํ„ฐ, ์ข…๋ฃŒ ํ›„ ๋นˆ ๋ชฉ๋ก โ€” ๋ชจ๋‘ ์ •์ƒ. - **b3(heartbeating โ†’ wall_clock)**: ๋‹จ๋… ์žฌ์‹คํ–‰์—์„œ๋„ ์‹คํŒจ. `subscriber.out` ์€ `idle timeout (3s, no events)` โ€” ์ด ์‹œํ—˜์€ `broker=True` ๋กœ **๊ณต๊ฐœ ๋ธŒ๋กœ์ปค**๋ฅผ ์ง€๋‚˜ 1 s heartbeat ๊ฐ€ 3 s idle ์ฐฝ ์•ˆ์— ๋„์ฐฉํ•ด์•ผ ๋ฒฝ์‹œ๊ณ„(6 s)๊ฐ€ ๋จผ์ € ์šธ๋ฆฐ๋‹ค. ์ด ํ˜ธ์ŠคํŠธ์˜ ๋ธŒ๋กœ์ปค ์ง€์—ฐ(~2 s, ์ŠคํŒŒ์ดํฌ 9 s โ€” ์ด์ „ ๋ฆฌ๋ทฐ๋“ค์˜ T-1 ๋ถ€๋ฅ˜)์—์„œ๋Š” idle ์ด ๋จผ์ € ์šธ๋ ค `deadline_timeout` ์ด ๋œ๋‹ค. **PR ๊ฒฐํ•จ์ด ์•„๋‹ˆ๋ผ ์‹œํ—˜ ์„ค๊ณ„์˜ ๋ธŒ๋กœ์ปค ์˜์กด**(M9)์ด์ง€๋งŒ, ์ด ์‹œํ—˜์ด #33 ์˜ ๋™๊ธฐ ์‚ฌ๋ก€๋ฅผ ๊ณ ์ •ํ•˜๋Š” ์œ ์ผํ•œ ์ž๋ฆฌ๋ผ ๊ฒฐ์ •์ ์œผ๋กœ ๋งŒ๋“ค ํ•„์š”๊ฐ€ ์žˆ๋‹ค(๋กœ์ปฌ events.log ๋กœ heartbeat ๋ฅผ ๋„ฃ๊ฑฐ๋‚˜ idle ์ฐฝ์„ ๋„‰๋„‰ํžˆ). - **send_keys_safe**: PR ๋ฏธ์ ‘์ด‰ ํŒŒ์ผ์˜ T-1 flake(๋‹จ๋… ์žฌ์‹คํ–‰ ํ†ต๊ณผ). - **issue14 `test_wrapper_honours_registry_ack_over_unconfirmed_inject`: ๋‹จ๋… ์žฌ์‹คํ–‰์€ ํ†ต๊ณผํ–ˆ์œผ๋‚˜ ํ•ฉ๋™ ์‹คํ–‰์—์„œ ๋ž˜ํผ๊ฐ€ ๋ฉˆ์ถฐ ์žˆ์—ˆ๋‹ค โ†’ N1.** ### 6-2. N1 (์ฐจ๋‹จ) โ€” ๋ž˜ํผ๊ฐ€ stall ๊ฐ์‹œ์ž๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋ฉฐ ์˜๊ตฌ ์ •์ง€, ์žก์€ `running` ์œผ๋กœ ๋‚จ๋Š”๋‹ค ์‹ค์ธก(๋ชจ๋‘ ์ฝ๊ธฐ ์ „์šฉ ๊ด€์ฐฐ; ๋งˆ์ง€๋ง‰ ํ•œ ๋ฒˆ๋งŒ **๋‚ด ์ƒŒ๋“œ๋ฐ•์Šค** ํ”„๋กœ์„ธ์Šค์— TERM ์„ ๋ณด๋ƒˆ๋‹ค): | ๊ด€์ฐฐ | ๊ฐ’ | |---|---| | ํ•ฉ๋™ ์‹คํ–‰ ๋’ค ํ˜ธ์ŠคํŠธ์— ๋‚จ์€ `lib_py.stall_watch` ํ”„๋กœ์„ธ์Šค | 2๊ฐœ โ€” ํ•˜๋‚˜๋Š” **๋‚ด** ์‹คํ–‰์˜ ์žก `d494fd13`(basetemp `p41-basetemp/test_wrapper_honours_registry_0`), ํ•˜๋‚˜๋Š” **๋‹ค๋ฅธ ๋ฆฌ๋ทฐ์–ด** ์‹คํ–‰์˜ ๊ฐ™์€ ์‹œํ—˜(`pytest-0/test_wrapper_honours_registry_0`, ์žก `b0e6a171`, 13๋ถ„ ๊ฒฝ๊ณผ) | | ๋‘ ๋ž˜ํผ(bash) ์˜ ์ƒํƒœ | ์‚ด์•„ ์žˆ๊ณ  `sample` ์ƒ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๊ฐ€ `__wait4` ์—์„œ ๋Œ€๊ธฐ, **์ž์‹์€ ๊ฐ์‹œ์ž ํ•˜๋‚˜๋ฟ**(๊ตฌ๋…์ž๋Š” ์ด๋ฏธ ์ข…๋ฃŒ) | | ์žก ๋ ˆ์ฝ”๋“œ | ๋‘˜ ๋‹ค `status: running`, `timeout_sec 6` โ€” ๋ฒฝ์‹œ๊ณ„ 6 s ๊ฐ€ ๋ช‡ ๋ถ„์ด ์ง€๋‚˜๋„ ๋‹ซ์ง€ ๋ชปํ•จ | | ๊ฐ์‹œ์ž ๋กœ๊ทธ | ๋‚ด ๊ฒƒ์€ ๋น„์–ด ์žˆ์Œ(์ •์ƒ ๋ฃจํ”„), ๋‹ค๋ฅธ ๋ฆฌ๋ทฐ์–ด ๊ฒƒ์€ `input box โ€ฆ not empty; nudge withheld` ๊ฐ€ 2 s ๊ฐ„๊ฒฉ์œผ๋กœ 116์ค„ โ€” ๊ฐ์‹œ์ž๋Š” ์ •์ƒ ๋™์ž‘ ์ค‘ | | ๋‚ด ๊ฐ์‹œ์ž์— `kill -TERM` | ์ฆ‰์‹œ ์ข…๋ฃŒ โ†’ **๋ž˜ํผ๋„ ์ฆ‰์‹œ ์ข…๋ฃŒ** | | ๊ฐ์‹œ์ž ๋‹จ๋… TERM ์‹คํ—˜(๊ฐ€์งœ herdr, ์‹œ์ž‘ ํ›„ 0.05/0.15/0.3/0.6/1/2 s) | ์ „๋ถ€ ์ข…๋ฃŒ(rc 143/0) โ€” ๊ฐ์‹œ์ž ์ž์ฒด๋Š” TERM ์„ ๋ฐ›๋Š”๋‹ค | ์ฆ‰ ๋ž˜ํผ๋Š” **๊ฐ์‹œ์ž ํ”„๋กœ์„ธ์Šค๊ฐ€ ๋๋‚˜๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๊ณ **, ๊ฐ์‹œ์ž๋Š” **์žก์ด ์ข…๋‹จ์ด ๋˜๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋ฉฐ**(`stall_watch.py:270-272` ๊ฐ€ ์œ ์ผํ•œ ์ •์ƒ ์ข…๋ฃŒ ์กฐ๊ฑด), ์žก์„ ์ข…๋‹จ์œผ๋กœ ๋งŒ๋“œ๋Š” ๊ฒƒ์€ ๊ทธ ๋ž˜ํผ๋‹ค โ€” ์ˆœํ™˜ ๋Œ€๊ธฐ. ๋ž˜ํผ๊ฐ€ ์–ด๋А `wait` ์— ์žˆ๋Š”์ง€๋Š” `sample` ์ด bash ์‹ฌ๋ณผ์„ ์ฃผ์ง€ ์•Š์•„ ๋ชป ๋ฐ•์ง€ ๋ชปํ–ˆ๋‹ค(`stop_stall_watch` ์˜ `wait "$STALL_PID"` ๊ฐ€ ์œ ๋ ฅํ•˜๋‚˜, ๊ทธ ์•ž์˜ `kill` ์ด ์™œ ๋‹ฟ์ง€ ์•Š์•˜๋Š”์ง€๋Š” ์žฌํ˜„ํ•˜์ง€ ๋ชปํ–ˆ๋‹ค). ๊ทธ๋Ÿฌ๋‚˜ ์›์ธ์ด ๋ฌด์—‡์ด๋“  **๋ฐฉ์–ด๊ฐ€ ์—†๋‹ค๋Š” ๊ฒƒ**์€ ์ฝ”๋“œ๋กœ ํ™•์ •๋œ๋‹ค: - `multi-agent-mux-delegate-job:706-719` `stop_stall_watch`: `kill` ๋’ค `wait "$STALL_PID"` ๊ฐ€ **๋ฌดํ•œ**์ด๋‹ค. ์ƒํ•œ๋„, `kill -9` ์Šน๊ฒฉ๋„ ์—†๋‹ค. TERM ์ด ํ•œ ๋ฒˆ์ด๋ผ๋„ ์œ ์‹คยท๋ฌด์‹œ๋˜๋ฉด ๋ž˜ํผ๋Š” ์˜์›ํžˆ ์„ ๋‹ค. - `stall_watch.py:263-312` `run()`: ๋ถ€๋ชจ ๋ž˜ํผ์˜ ์ƒ์กด์„ ๋ณด์ง€ ์•Š๋Š”๋‹ค(#18 heartbeat ํƒ€์ด๋จธ์˜ `--watch-pid` ๊ฐ™์€ ๊ฒฐ์†์ด ์—†๋‹ค). ์žก์ด ์ข…๋‹จ์ด ๋˜์ง€ ์•Š๋Š” ํ•œ(๋ž˜ํผ๊ฐ€ ์ฃฝ๊ฑฐ๋‚˜ ๋ฉˆ์ถ”๋ฉด ์˜์›ํžˆ) herdr ๊ฐ€ ์‚ด์•„ ์žˆ๋Š” ๋™์•ˆ ๊ณ„์† ๋ˆ๋‹ค. - ๊ฒฐ๊ณผ๋Š” ๊ฐฑ์‹  1 ์˜ "์ข…๊ฒฐ์€ ๋ฒฝ์‹œ๊ณ„ยท๋ฌด์‘๋‹ต ๊ฒฝ๋กœ์—๋งŒ" ๊ณผ ์ •๋ฐ˜๋Œ€๋‹ค: ๋ฒฝ์‹œ๊ณ„ ๊ฒฝ๋กœ ์ž์ฒด๊ฐ€ ์‹คํ–‰๋˜์ง€ ๋ชปํ•ด ์žก์ด **๋‹ซํžˆ์ง€ ์•Š๋Š”๋‹ค**. ๋ฃจํ”„ ์ชฝ์€ `budget+300` ๋’ค `wait_for_job` ์ด ํฌ๊ธฐํ•˜์ง€๋งŒ ๋ž˜ํผยท๊ฐ์‹œ์žยท`running` ๋ ˆ์ฝ”๋“œ๋Š” ๋‚จ๊ณ , #34 ํ‘œ์—๋Š” OPEN ์œผ๋กœ ์˜๊ตฌํžˆ ๋ณด์ธ๋‹ค. ๊ฐ™์€ ์‹œํ—˜์ด ์ด PR ์ด์ „(#37ยท#38 ๋ฆฌ๋ทฐ์˜ ๊ฐ™์€ ํ˜ธ์ŠคํŠธ)์—๋Š” ํ•œ ๋ฒˆ๋„ ๋ฉˆ์ถ˜ ์ ์ด ์—†๊ณ , ์˜ค๋Š˜ ๋‘ ๋ฆฌ๋ทฐ์–ด์˜ ํ•ฉ๋™ ์‹คํ–‰์—์„œ ๊ฐ๊ฐ ๋ฉˆ์ท„๋‹ค. ๋‹จ๋… ์žฌ์‹คํ–‰์ด ํ†ต๊ณผํ•˜๋ฏ€๋กœ ๋ถ€ํ•˜ยทํƒ€์ด๋ฐ ์˜์กด์ด์ง€๋งŒ, ์œ„์ž„ ๋ž˜ํผ์˜ ์˜๊ตฌ ์ •์ง€๋Š” ๊ฐ•๋„๊ฐ€ ๋†’์•„ **์ฐจ๋‹จ**์œผ๋กœ ๋ถ„๋ฅ˜ํ•œ๋‹ค. โ†’ **๋‹ซ๋Š” ๋ฐฉ๋ฒ•(์ž‘์Œ)**: (1) `stop_stall_watch` ๋ฅผ ์œ ๊ณ„๋กœ โ€” `kill`, ์ตœ๋Œ€ N s(์˜ˆ: 5) ํด๋ง ๋Œ€๊ธฐ, ๋‚จ์œผ๋ฉด `kill -9`, ๊ทธ ๋’ค `wait`; (2) ๊ฐ์‹œ์ž์— `--watch-pid <๋ž˜ํผ pid>` ๋ฅผ ์ฃผ๊ณ  `run()` ๋ฃจํ”„ ๋จธ๋ฆฌ์—์„œ ๋ถ€๋ชจ๊ฐ€ ์‚ฌ๋ผ์ง€๋ฉด rc 0 ์œผ๋กœ ๋๋‚˜๊ฒŒ(heartbeat ํƒ€์ด๋จธ์™€ ๊ฐ™์€ ๊ณ„์•ฝ); (3) S9 ์— "๊ฐ์‹œ์ž๊ฐ€ TERM ์„ ๋ฌด์‹œํ•ด๋„ ๋ž˜ํผ๋Š” N s ์•ˆ์— ๋Œ์•„์˜จ๋‹ค" ํšŒ๊ท€๋ฅผ ์ถ”๊ฐ€. (1)๋งŒ์œผ๋กœ๋„ ์ •์ง€๋Š” ์‚ฌ๋ผ์ง€๊ณ , (2)๊ฐ€ ๊ณ ์•„๋ฅผ ์—†์•ค๋‹ค. ### 6-3. ์ˆ˜์ • ์—†์ด๋Š” ์‹คํŒจํ•˜๋Š”๊ฐ€ ๊ฒ€์ฆ ๊ฒฝ๋กœ ์‚ฌ๋ณธ์—์„œ PR ์ด ๋ฐ”๊พผ ์†Œ์Šค 14ํŒŒ์ผ์„ `8ea65bb` ํŒ์œผ๋กœ ๋˜๋Œ๋ฆฌ๊ณ (์ƒˆ ๋ชจ๋“ˆ `stall_watch.py` ๋Š” ๊ทธ๋Œ€๋กœ) ์ƒˆ ํŒŒ์ผ 2๊ฐœ๋ฅผ ๋Œ๋ฆฐ ๊ฒฐ๊ณผ **14 failed / 6 passed / 48 skipped**: B1(env ๊ฒ€์ฆ)ยทB2(์˜ˆ์‚ฐ ์ „๋‹ฌยท๋Œ€๊ธฐ ์ƒํ•œ)ยทB3ยทB4(์›๊ฒฉ ๋ผ๋ฒจ)ยทB5(#34 ํ‘œ)ยทR3 ๊ฐ€ ์‹คํŒจ โ€” #33 ํšŒ๊ท€๋Š” ์ •์ •์„ ์ •ํ™•ํžˆ ๊ฒจ๋ƒฅํ•œ๋‹ค. #30 ํŒŒ์ผ์€ Linux ์ „์šฉ ๋งˆํฌ๋กœ ์‚ฌ๋ณธ์—์„œ๋„ skip ์ด๋ผ ์ด ๋ฐฉ๋ฒ•์œผ๋กœ๋Š” ํŒ์ •ํ•  ์ˆ˜ ์—†๋‹ค(์ƒˆ ๋ชจ๋“ˆ์ด๋ฏ€๋กœ ๋ชจ๋“ˆ ๋ถ€์žฌ = ์ „๋ถ€ ์‹คํŒจ, ์ž๋ช…). ### 6-4. ๊ฒฉ๋ฆฌ ์‹ค์ œ `.mam/jobs`ยท`.mam/delegate_job_logs` ์ „ํ›„ **์ฐจ์ด 0 / ์ฐจ์ด 0**. ๊ฒ€์ฆ ๊ฒฝ๋กœ tracked ํŒŒ์ผ ๋ณ€ํ™” ์—†์Œ(HEAD `1673818`), ๋ณธ ์ฒดํฌ์•„์›ƒ `main 8ea65bb`. ์‹ค์ œ ์„ธ์…˜ยทherdr ์„œ๋ฒ„ ์ ‘์ด‰ ์—†์Œ. ๋‹จ, ์œ„ N1 ์˜ **๋‹ค๋ฅธ ๋ฆฌ๋ทฐ์–ด์˜** ๋ฉˆ์ถ˜ ๋ž˜ํผยท๊ฐ์‹œ์ž(pid 11266/12518, `pytest-0/...`)๋Š” ๋‚ด ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ๊ทธ๋Œ€๋กœ ๋‘์—ˆ๋‹ค โ€” ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ๊ฐ€ ์ •๋ฆฌํ•  ๊ฒƒ. ## 9. ๋น„์ฐจ๋‹จ ๊ด€์ฐฐ | # | ๊ด€์ฐฐ | ๊ทผ๊ฑฐ | |---|---|---| | M1 | ๋””์Šคํฌ heartbeat ์œ ์˜ˆยทbroker ์žฌ๊ด€์ธก ๋ฃจํ”„์˜ ์‹œ๊ณ„ ํŒ์ •(`FINALIZE_CLOCK=wall`)์ด bash ์˜ ๊ฒฐ์ • | ๋ž˜ํผ `:902`, `:950`; ํ•ญ๋ชฉ 4 ์ด์ „ ๋Œ€์ƒ | | M2 | ๊ฐ์‹œ ์—ฌ๋ถ€(remote/human/`MAM_STALL_WATCH`) ํŒ์ •์ด bash | `:684-702`; ํ•ญ๋ชฉ 4 ์ด์ „ ๋Œ€์ƒ | | M3 | ๊ฐ์‹œ์ž ์ˆ˜๋ช…์ด ๋ž˜ํผ์˜ ์ •์ƒ ๊ฒฝ๋กœ์—๋งŒ ๋ฌถ์—ฌ ์žˆ๊ณ  EXIT trap ์ด ์—†๋‹ค โ€” N1 ์˜ ๊ตฌ์กฐ์  ๋ฐฐ๊ฒฝ | `:370-385`, `:706-719` | | M4 | `MAM_JOB_TIMEOUT_S` ๊ฐ€ ์ž˜๋ชป๋œ ๊ฐ’์ผ ๋•Œ ๋ž˜ํผ๋Š” ๊ฒฝ๊ณ , ๋Ÿฌ๋„ˆ๋Š” ๋ฌด์–ธ | `runner.py:595-600` | | M5 | delegate-job SKILL.md `:100` "before v4.7.5" โ€” ๋ณ‘ํ•ฉ ๋ฒ„์ „์€ 4.8.0 | ๋ฌธ์„œ | | M6 | `MAM_WAIT_GRACE_S` ๋ฏธ๋ฌธ์„œ | `runner.py:608` | | M7 | `MAM_TEST_UNRELIABLE_AGENT` ํ…Œ์ŠคํŠธ ํ›…์ด ์ œํ’ˆ ์ฝ”๋“œ์— ์žˆ์Œ | `stall_watch.py:228` | | M8 | `test_issue30_stall_watch.py:39` ๋ชจ๋“ˆ ๋งˆํฌ๊ฐ€ `/proc` ๋ฌด๊ด€ 29๊ฑด๊นŒ์ง€ skip | ยง6-1 | | M9 | B3 heartbeating ์‹œํ—˜์ด ๊ณต๊ฐœ ๋ธŒ๋กœ์ปค ์ง€์—ฐ์— ์˜์กด(์ด ํ˜ธ์ŠคํŠธ 2/2 ์‹คํŒจ) | ยง6-1 | ๋ฒ„์ „ ๋ฝ์Šคํ… bump ๋ถ€์žฌ(`lib.sh:32` `4.7.4`, 4.8.0 ์˜ˆ์ •)๋Š” ๋ธŒ๋ฆฌํ”„๋Œ€๋กœ ํŒ์ •์—์„œ ์ œ์™ธํ•œ๋‹ค. ## 10. ๊ต์ฐจ๊ฒ€์ฆ ๊ฐ€์ด๋“œ ```bash RP=/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260926T025230Z-a30a6d/1 PY=/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.venv/bin/python git -C "$RP" rev-parse HEAD 'HEAD^{tree}' # 1673818โ€ฆ / 00d5e03โ€ฆ # N1: ํ•ฉ๋™ ์‹คํ–‰ ๋’ค ๋‚จ์€ ๊ฐ์‹œ์ž์™€ ๊ทธ ๋ถ€๋ชจ ๋ž˜ํผ ps -eo pid=,ppid=,stat=,etime=,args= | grep '[l]ib_py.stall_watch' # ๋ถ€๋ชจ bash ๊ฐ€ __wait4 ์—์„œ ๋Œ€๊ธฐ, ์žก ๋ ˆ์ฝ”๋“œ running # ๊ฐ์‹œ์ž์— TERM โ†’ ๋ž˜ํผ๊ฐ€ ์ฆ‰์‹œ ํ’€๋ฆผ (์ž๊ธฐ ์ƒŒ๋“œ๋ฐ•์Šค ํ”„๋กœ์„ธ์Šค์—๋งŒ) # ์˜ˆ์‚ฐ/๋ผ๋ฒจ: --dry-run ์œผ๋กœ timeout_sec ํ™•์ธ, in-process ๋กœ job_budget/job_wait_s/contract_args (ยง1) # ๋˜๋Œ๋ฆฐ ์‚ฌ๋ณธ: ์†Œ์Šค 14ํŒŒ์ผ์„ 8ea65bb ํŒ์œผ๋กœ โ†’ test_issue33_budget.py โ†’ 14 failed / 6 passed ``` [VERDICT: NOT PASS]