เอเจนต์สิบตัวรัน typecheck เดียวกันพร้อมกัน ทางแก้คือโฟลเดอร์หนึ่งอัน
เอเจนต์เขียนโค้ดสิบเจ็ดตัวใน checkout เดียวกัน tsc สิบตัวพร้อมกัน load average 37 และ RAM ว่าง 87 MB typecheck ที่ปกติใช้เก้าสิบวินาทีกินเวลาไป 7 นาที 36 วินาที นี่คือตัวเลขที่วัดได้ เหตุผลที่เครื่องไม่ได้กำลังคำนวณ และล็อกร่วมเล็กๆ ที่แก้ปัญหาได้ คัดลอกไปใช้กับรีโปไหนก็ได้
วันที่ 7 กันยายน เครื่องพัฒนาของเราหยุดตอบสนองอย่างถูกต้อง ไม่ใช่การแครช ไม่ใช่การค้าง ทุกอย่างแค่ใช้เวลานานขึ้นสิบเท่า รวมถึงสิ่งที่ไม่เกี่ยวกับโค้ดเลย
ผู้ต้องสงสัยที่ชัดเจนผิดหมดทุกข้อ โน้ตบุ๊กไม่ได้ร้อนเกิน ไม่มีการบันทึก throttling แบตเตอรี่อยู่ที่ 30.6 C ไม่มีโปรเซสไหนวิ่งหลุดกินซีพียู ไม่มีการดีพลอยอะไรทั้งนั้น สิ่งเดียวที่ผิดปกติคือมี CLI ของเอเจนต์สิบเจ็ดตัวยังทำงานอยู่ในรีโปเดียวกัน ซึ่งเป็นวันทำงานปกติของที่นี่
นี่คือสิ่งที่เกิดขึ้นจริง วัดมา ไม่ได้เดา
ตัวเลขที่วัดได้
เครื่อง 16 GB 8 คอร์ เปิดมาห้าชั่วโมงครึ่ง เอเจนต์สิบเจ็ดตัวกำลังทำงาน
| สิ่งที่เราวัด | ค่า |
|---|---|
| CLI ของเอเจนต์ที่ยังมีชีวิต | 17 |
โปรเซส tsc --noEmit ที่ทำงานพร้อมกัน เห็นภายในสองนาที | 3 แล้วเป็น 10 |
| Load average | 37 ถึง 41 |
| RAM ว่าง / ตัวบีบอัดหน่วยความจำ | 87 MB / 7.2 GB |
| typecheck หนึ่งครั้งบนแอปเดสก์ท็อป ตอนเครื่องตัน | 7 นาที 36 วินาทีตามนาฬิกา ต่อ CPU 26 วินาที |
| typecheck เดียวกัน ตอนเครื่องว่าง | 33 วินาที |
บรรทัดที่ชี้ขาดคือบรรทัดรองสุดท้าย ซีพียูยี่สิบหกวินาทีที่ถูกยืดออกไปเจ็ดนาทีครึ่ง คือการใช้งานสิบเปอร์เซ็นต์ typecheck ไม่ได้กำลังคำนวณ มันกำลังรอหน่วยความจำ
และหนึ่งในโปรเซสเหล่านั้นถูกระบบปฏิบัติการฆ่าทิ้งกลางทาง tsc ที่ถูกฆ่าจะออกด้วยโค้ดที่ไม่ใช่ศูนย์และเอาต์พุตว่างเปล่า ซึ่งแยกไม่ออกจากข้อผิดพลาดของชนิดข้อมูลจริงๆ ดังนั้นนอกจากจะช้าแล้ว เครื่องยังผลิตคำตัดสินที่ไม่มีใครเชื่อถือได้ออกมาด้วย
ไม่มีใครทำอะไรผิดเลย
นี่คือส่วนที่ควรหยุดคิด เพราะมันคือสิ่งที่ทำให้ความพังครั้งนี้มองเห็นล่วงหน้าได้ยากมาก
เอเจนต์ทุกตัวทำตามกฎ ทุกตัวแก้ TypeScript ทุกตัวถูกสั่งให้ตรวจชนิดข้อมูลก่อนคืนงาน ทุกตัวรัน tsc --noEmit ไม่มีตัวไหนมองเห็นตัวอื่นเลย ไม่มีกระดานกลางที่เอเจนต์จะเขียนลงไปว่า "ตอนนี้ฉันกำลังทำของแพงอยู่ รอก่อนนะ"
จากนั้นมันก็เลี้ยงตัวเอง typecheck ช้าลงเพราะเครื่องตัน เอเจนต์ที่เฝ้าดูมันสรุปว่ามันค้าง มันเลยฆ่าทิ้งแล้วเริ่มตัวใหม่ ปฏิกิริยานี้ถูกต้องเมื่ออยู่ตัวเดียว และหายนะเมื่ออยู่เป็นกลุ่ม และมันคือความพังตระกูลเดียวกับที่เราบันทึกไว้เมื่อเดือนก่อน ตอนที่เอเจนต์ทิ้งโปรเซสค้นหาที่ค้างเอาไว้ข้างหลัง Process Guard คือตาข่ายที่ไล่หาสิ่งที่เริ่มไปแล้ว ส่วนนี่คือทางแก้ที่หยุดไม่ให้มันเริ่ม
สามคำตอบที่เราไม่เลือก
รันเอเจนต์น้อยลง วิธีนี้ลดอาการลงครึ่งหนึ่งแต่เก็บบั๊กไว้ครบ typecheck สองตัวพร้อมกันบนเครื่องที่โหลดหนักก็ยังช้ากว่าตัวเดียว และการหั่นกองเอเจนต์ลงคือการจ่ายค่าปัญหาด้วยสิ่งที่ทำให้งานเร็ว
typecheck ครั้งเดียวตอนจบ น่าสนใจ แต่ผิด ด้วยเหตุผลที่ไม่เกี่ยวกับประสิทธิภาพเลย ข้อผิดพลาดของชนิดข้อมูลที่เจอตอนผ่านไปสิบทิกเก็ตแล้วคือเด็กกำพร้า เอเจนต์ที่เขียนมันปิดไปแล้ว บริบทของมันหายไปแล้ว และมนุษย์ต้องเปิดเรื่องทั้งเรื่องขึ้นมาใหม่เพื่อแก้บรรทัดเดียว เราไม่อยากเลื่อนการตรวจสอบออกไป
การคอมไพล์แบบเพิ่มทีละส่วน ทดลองแล้วและตัดทิ้ง ผลได้น่าสงสัยในโหมด --noEmit และโปรเซสที่ทำงานพร้อมกันทำให้ .tsbuildinfo ที่ใช้ร่วมกันเสียหาย มันแก้ปัญหาไปครึ่งหนึ่งด้วยการทำให้อีกครึ่งแย่ลง
สิ่งที่เราทำแทน คือเช็กครั้งเดียว แล้วแบ่งกันใช้
กฎไม่ใช่ "ตรวจให้น้อยลง" แต่คือ typecheck ทีละหนึ่งครั้ง ต่อหนึ่งโปรเจกต์ สำหรับทุกคน สคริปต์ห่อหุ้มแทนที่การตรวจ N ครั้งด้วยครั้งเดียว และตอบสามกรณี
- ไม่มีอะไรเปลี่ยนตั้งแต่รันครั้งก่อน ก็คืนผลลัพธ์ของครั้งนั้นไป
- มีรันหนึ่งกำลังทำงานอยู่แล้ว ก็รอมัน แล้วเอาผลของมันไป
- ถ้าไม่ใช่ทั้งสองอย่าง ก็ถือล็อกและเป็น
tscตัวเดียวบนเครื่อง
จากมุมของเอเจนต์ ไม่มีอะไรเปลี่ยน มันพิมพ์ yarn typecheck มันได้ข้อผิดพลาดของชนิดข้อมูลกลับมา และมันก็ไม่เคยรอนานกว่าเดิมด้วย เพราะรันที่มันรออยู่ข้างหลังคือรันที่เริ่มก่อนรันของมันเอง เครื่องจ่ายค่าหนึ่งครั้งแทนที่จะสิบครั้ง
นั่นคือไอเดียทั้งหมด ส่วนที่น่าสนใจคือกลไกสองอย่างที่มันต้องใช้นั้นเล็กกว่าที่คิดกันมาก
ล็อกคือโฟลเดอร์
ไม่ใช่ไฟล์ ไม่ใช่ฐานข้อมูล ไม่ใช่เดมอน แต่เป็นโฟลเดอร์
try {
mkdirSync(lockDir); // สำเร็จ แปลว่าล็อกเป็นของเรา
} catch (err) {
if (err.code === 'EEXIST') { /* มีคนอื่นถืออยู่ รอก่อน */ }
}
mkdir จะสร้างโฟลเดอร์ได้ หรือไม่ก็ล้มเหลวด้วย EEXIST และมันทำแบบอะตอมมิกทั้งบน macOS, Windows และ Linux โดยไม่ต้องพึ่งไลบรารีใดและไม่ต้องเรียกโค้ดเนทีฟ การเขียนไฟล์แล้วค่อยเช็กว่ามันมีอยู่ไหมจะกลายเป็นสองปฏิบัติการ และสองปฏิบัติการคือจุดที่เอเจนต์ตัวที่สองแทรกเข้ามาได้พอดี
ในโฟลเดอร์นั้นเราวาง owner.json ที่มี pid ชื่อโฮสต์ และเวลาเริ่ม ไฟล์นั้นมีไว้เพื่อการวินิจฉัยและเพื่อตรวจจับล็อกที่ตายแล้ว ไม่ใช่ตัวที่กันคิวเลย
ล็อกที่ตายแล้วจะถูกยึดคืนอัตโนมัติในสองกรณี คือโปรเซสเจ้าของหายไปแล้ว (ตรวจเฉพาะเมื่อชื่อโฮสต์ตรงกัน เพราะ pid ไม่มีความหมายข้ามเครื่อง) หรือล็อกเก่ากว่าสิบห้านาที
กับดักหนึ่งที่ทำให้เราเสียบั๊กไปหนึ่งตัว ระหว่าง mkdir กับการเขียน owner.json มีช่องเวลาหนึ่งที่อ่านเจ้าของไม่ได้ การประกาศว่าล็อกตายในช่องนั้นคือการขโมยมันไปจากโปรเซสที่เพิ่งถือไปหมาดๆ ซึ่งเป็นการแข่งกันเข้าถึงแบบเดียวกับที่ไฟล์นี้มีไว้เพื่อป้องกันพอดี ดังนั้นเมื่ออ่านเจ้าของไม่ได้ เราจะตัดสินจากอายุของโฟลเดอร์ ไม่ใช่จากไฟล์ที่หายไป
ลายนิ้วมือคือวันที่กับจำนวน
กรณีที่ 1 ต้องรู้ว่ามีอะไรเปลี่ยนไปตั้งแต่รันครั้งก่อนหรือเปล่า คำตอบที่ชัดเจนคือแฮชไฟล์ต้นฉบับ แต่เราไม่ทำแบบนั้น
ลายนิ้วมือคือ <mtime ล่าสุด>:<จำนวนไฟล์> บนรากที่อนุมานจากช่อง include ของ tsconfig บวกกับตัว tsconfig เอง
บนไฟล์ 2,300 ไฟล์ การอ่านทุกไบต์แพงกว่าการตรวจที่มันช่วยประหยัดเสียอีก วันที่อย่างเดียวจะมองไม่เห็นการลบ จำนวนอย่างเดียวจะมองไม่เห็นการแก้ไข พอรวมกันแล้วมันครอบคลุมทั้งสองอย่าง ผลลบลวงที่เรายอมรับคือการแก้สองครั้งภายในมิลลิวินาทีเดียวกันที่ทำให้จำนวนไฟล์เท่าเดิม และกรณีเลวร้ายที่สุดของมันคือผลลัพธ์จากแคชที่เก่าไปไม่กี่วินาที ไม่เคยเป็นข้อผิดพลาดของชนิดข้อมูลที่เงียบไป เพราะการตรวจสอบที่บล็อกจริงๆ ยังเป็นตัวบิลด์อยู่ดี
กฎที่เขียนไว้ยังไม่พอ เราเลยเพิ่ม hook
คำสั่งอยู่ใน AGENTS.md ตั้งแต่วันแรก อย่ารัน tsc ตรงๆ ให้ใช้สคริปต์ร่วมเสมอ มันยังไม่พอ และควรพูดกันตรงๆ ว่าทำไม
การตรวจชนิดข้อมูลหลังแก้โค้ดคือปฏิกิริยาที่ฝังลึกมาก ภายใต้แรงกดดัน เอเจนต์จะพิมพ์ npx tsc --noEmit โดยไม่อ่านคำสั่งซ้ำ และแค่เอเจนต์ ตัวเดียว ที่ข้ามกฎ ก็สร้างการกองทับกันที่ล็อกมีไว้เพื่อป้องกันขึ้นมาใหม่ได้แล้ว คำสั่งนั้นต่อรองได้ hook ต่อรองไม่ได้
เราจึงต่อ hook แบบ PreToolUse เข้ากับเครื่องมือ Bash ที่ปฏิเสธ tsc ตรงๆ และบอกชื่อคำสั่งที่ถูกต้องไว้ในข้อความปฏิเสธ
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/scripts/hooks/block-direct-tsc.mjs\"" }
]
}
]
}
}
hook อ่านการเรียกเครื่องมือเป็น JSON จาก stdin ออกด้วยโค้ด 2 พร้อมเหตุผลบน stderr เพื่อปฏิเสธ ออกด้วยโค้ด 0 เพื่อปล่อยผ่าน มีสองรายละเอียดที่ทำให้ hook ที่มีประโยชน์ต่างจาก hook ที่น่ารำคาญ
มันจับ tsc ในตำแหน่งของคำสั่ง ไม่ใช่ที่ไหนก็ได้ในสตริง การไล่หาสามตัวอักษรนั้นทุกที่จะไปปฏิเสธ grep -rn tsc AGENTS.md ด้วย รูปแบบจึงกำหนดให้ tsc อยู่ต้นบรรทัด หรือหลัง ; && || | หรือ ( โดยอาจมีตัวรันแพ็กเกจและพาธนำหน้าได้ และมันปล่อย tsc --version ผ่านด้วย เพราะไม่มีเหตุผลที่จะปฏิเสธออปชันที่แค่บอกข้อมูล
มันปฏิเสธอีกอย่างหนึ่งที่เราคาดไม่ถึง เราเห็นเอเจนต์ตัวหนึ่งรออยู่หลังล็อก แล้วสรุปหลังจากผ่านไปพักหนึ่งว่าล็อกคงตายแล้ว จึงลบโฟลเดอร์ล็อกทิ้งเพื่อปลดบล็อกตัวเอง นั่นคือการเริ่มโปรเซสหนักตัวที่สองขึ้นมาข้างตัวที่ยังทำงานอยู่ ซึ่งเป็นการเลี่ยงทุกอย่างที่ล็อกปกป้องไว้อย่างสมบูรณ์แบบ การลบโฟลเดอร์ล็อกหรือโฟลเดอร์แคชจึงถูกปฏิเสธด้วยเช่นกัน พร้อมคำอธิบายว่าล็อกที่ตายแล้วจะถูกยึดคืนได้เอง
การปฏิเสธข้อที่สองนี้เป็นสิ่งที่เราคงไม่มีวันเขียนไว้ล่วงหน้า มันมาจากการเฝ้าดูว่าเอเจนต์ทำอะไรจริงๆ เมื่อมันติดขัด ซึ่งเป็นแหล่งที่มาของราวกันตกที่ดีกว่าการนั่งจินตนาการว่ามันอาจจะทำอะไร
จุดที่มันหยุด
hook ผูกกับ Claude Code ส่วน CLI ของเอเจนต์ตัวอื่นในกองเห็นแค่กฎที่เขียนไว้ นั่นคือช่องโหว่ที่เรารู้ตัวและยอมรับ ราวกันตกที่ครอบคลุมกองเอเจนต์ส่วนใหญ่ยังดีกว่าไม่มีราวกันตกเลยระหว่างรอมาตรฐาน hook ที่ CLI ทุกตัวอ่านได้
ส่วนตัวสคริปต์ร่วมนั้นเป็นกลางต่อผู้ให้บริการ เพราะมันก็แค่คำสั่งหนึ่ง CLI ตัวไหนก็ตามที่รัน yarn typecheck ได้ ก็ได้ประโยชน์จากล็อก ไม่ว่าจะมีอะไรบังคับมันหรือไม่
สิ่งที่ควรเก็บกลับไป
typecheck เป็นกรณีที่เสียงดังที่สุดของเรา ไม่ใช่กรณีพิเศษ รูปแบบนี้ใช้ได้กับคำสั่งใดก็ตามที่แพง ให้ผลเหมือนเดิมภายในช่วงเวลาสั้นๆ และถูกเอเจนต์ทุกตัวเริ่มด้วยเหตุผลดีๆ เดียวกัน เช่น การติดตั้งดีเพนเดนซี การรันชุดทดสอบทั้งชุด การบิลด์สำหรับโปรดักชัน การเปิดเซิร์ฟเวอร์ dev บนพอร์ตตายตัว
สามคำถาม เรียงตามลำดับนี้ แล้วคุณจะได้การออกแบบทั้งหมด
- ฉันใช้ผลลัพธ์ล่าสุดซ้ำได้ไหม
- ฉันไปเกาะรันที่กำลังทำงานอยู่ได้ไหม
- ถ้าไม่ได้ ฉันคือตัวที่เริ่มมันเอง ตัวเดียว ใช่ไหม
ถ้ามีเอเจนต์หลายตัวใช้เครื่องของคุณร่วมกัน สิ่งที่ควรวัดไม่ใช่ว่ามีกี่ตัวกำลังทำงานอยู่ แต่คือ มีกี่ตัวที่เริ่มคำสั่งเดียวกันในนาทีเดียวกัน ตัวเลขนั้นคือสิ่งที่เครื่องของคุณรู้สึกจริงๆ และตราบใดที่คุณยังไม่มองมัน คุณก็จะโทษความร้อนต่อไป
ถ้าอยากได้ภาพรวมกว้างขึ้นว่าเรารันเอเจนต์หลายตัวบนรีโปเดียวกันโดยไม่ให้เหยียบกันได้อย่างไร อยู่ใน การรันเอเจนต์เขียนโค้ดแบบขนาน และตาข่ายนิรภัยสำหรับโปรเซสที่เริ่มไปแล้วจริงๆ คือ Process Guard ทั้งสคริปต์ร่วมและ hook ต่างก็อยู่ในรีโปของ AgentsRoom ซึ่งก็คือที่ที่เอเจนต์สิบเจ็ดตัวนั้นทำงานกันอยู่ในบ่ายวันนั้น
ดาวน์โหลด AgentsRoom
รันเอเจนต์ AI ทั้งหมดของคุณในทุกโปรเจกต์ จากหน้าต่างเดียว
แอปคู่หู: ตรวจสอบเอเจนต์ของคุณได้ทุกที่
นำของคุณเอง: Claude, Codex, Antigravity CLI, หรือผู้ให้บริการ AI อื่น ๆ
ส่งข้อบกพร่องและคำขอไปยังแบ็คล็อกสาธารณะของคุณโดยตรง
มองเห็น AgentsRoom ในการทำงาน
อ่านต่อ
hook events 30 ตัวทำงานในหนึ่งเซสชันของ Claude Code มีแค่ 3 ตัวที่พูดกลับได้
รายการ hook events ทั้งหมดของ Claude Code ว่าแต่ละตัวทำงานตอนไหน ตัวไหนใน 15 ตัวที่บล็อกได้ และกฎของ stdout ที่กลืนเอาต์พุตของ hook ส่วนใหญ่ไปอย่างเงียบ ๆ เอกสารอ้างอิงจากหน้างานจริง ที่สร้างจากการรัน hook ในโปรดักชันข้ามเซสชันเอเจนต์นับพัน
อ่านบทความAgentsRoom รองรับ Grok Build แล้ว
Grok Build เอเจนต์การเขียนโค้ดในเทอร์มินัลของ xAI ตอนนี้เป็นผู้ให้บริการระดับหนึ่งใน AgentsRoom ใช้งานร่วมกับ Claude, Codex, Antigravity CLI, OpenCode และ Aider และสลับระหว่างการสนทนาได้
อ่านบทความโค้ชวิ่ง AI ของผมคือคลัง Git กับเอเจนต์ Claude
ผมวิ่งจบ นาฬิกาซิงก์ข้อมูลเอง แล้วสามนาทีต่อมาบทวิเคราะห์ก็ถูกเขียนลงในคลังของผม ตารางสัปดาห์ถูกปรับใหม่ และโค้ชได้ทิ้งคอมเมนต์ไว้ใต้กิจกรรม Strava แล้ว ไม่ได้เขียนแอป ไม่ได้เขียนเซิร์ฟเวอร์ ไม่มีบิล API คิดตาม token: มีแค่ซับสคริปชัน Claude, AgentsRoom และไฟล์ Markdown นี่คือวิธีประกอบทั้งหมด ทำตามได้จริง
อ่านบทความ