Octofs 0.14: การรอไม่ใช่ tool call
เอเจนต์รันชุดเทสต์ ชุดเทสต์ใช้เวลาสี่นาที ส่วน idle timeout ของ MCP client อยู่ที่หกสิบวินาที
คุณคงเดาออกว่าเรื่องนี้จะจบยังไง พอถึงวินาทีที่หกสิบ ไคลเอนต์ก็ยกเลิก call ทิ้ง โพรเซสยังรันต่อไป — ไม่มีใครสั่งให้มันหยุด — ขณะที่โมเดลซึ่งถือใบยกเลิกอยู่ในมือแทนที่จะเป็นผลเทสต์ ก็ทำสิ่งที่สมเหตุสมผลที่สุด: รันชุดเทสต์อีกรอบ ชุดเทสต์สองชุด ไดเรกทอรีเดียวกัน แข่งกันแย่ง build artifact ชุดเดียวกัน รอบที่สองล้มเหลวด้วย locking error โมเดลรายงานว่าเทสต์พัง ทั้งที่เทสต์ไม่ได้พังเลย
ในอีกเซสชันหนึ่ง โมเดลตัวเดิมที่เคยเจ็บมาก่อน ก็พัฒนาทางลัดของตัวเองขึ้นมา: รันบิลด์ แล้วเรียก sleep 240 แล้วค่อยไปดู tool call ที่ไม่ทำอะไรเลย ถูกเปิดค้างไว้สี่นาที เพียงเพื่อให้ tool call อีกตัวอาจมีอะไรให้ดูบ้าง โมเดลได้ประดิษฐ์ polling ขึ้นมาใหม่แบบห่วย ๆ เพราะเราไม่เคยยื่นอะไรที่ดีกว่านั้นให้มันเลย
คราวก่อนที่เราเขียนถึง octofs หัวข้อคือการแก้ไขที่ลงผิดบรรทัด บทความนั้นปิดท้ายด้วยหลักการหนึ่ง: อินเทอร์เฟซที่แท้จริงของ MCP server คือทุกสตริงที่มันส่งกลับไปให้โมเดล สิบเอ็ดรีลีสหลังจากนั้น — 0.10.1 ถึง 0.14.1 ในสองสัปดาห์ — นำหลักการเดียวกันมาใช้กับสตริงที่ช้าที่สุดในบรรดาทั้งหมด: สตริงที่โมเดลต้องนั่งรอ ตอนนี้ shell เป็นแบบ event-driven แล้ว คำสั่งเริ่มต้นใน foreground ย้ายตัวเองไป background ถ้ารันเกินสิบวินาที และไคลเอนต์จะได้รับ notification เมื่อคำสั่งจบ ไม่มีอะไรบล็อก ไม่มีอะไรถูกฆ่าทิ้ง ไม่มีอะไรรันซ้ำสอง
นี่คือเส้นทางที่พาเรามาถึงจุดนี้ รวมทางที่เลี้ยวผิดไว้ให้ด้วย
แก้ครั้งแรก: พิสูจน์ว่า call ยังมีชีวิตอยู่
การถูกยกเลิกที่วินาทีหกสิบมีสาเหตุตื้นหนึ่งข้อและสาเหตุลึกอีกหนึ่งข้อ สาเหตุตื้น: shell call เงียบโดยธรรมชาติ บิลด์ที่กำลังคอมไพล์ไม่ส่งอะไรออกมาบนสายเลยเป็นนาที ๆ และสำหรับ MCP client ความเงียบนั้นแยกไม่ออกจากเซิร์ฟเวอร์ที่ค้างไปแล้ว 0.10.2 จึงเพิ่ม liveness heartbeat — ระหว่างที่คำสั่งรันอยู่ใน foreground octofs จะส่ง progress notification ทุกสิบวินาที ต่ำกว่า idle timeout ที่สมเหตุสมผลใด ๆ อยู่มาก จังหวะที่หลุดไปครั้งเดียวจึงไม่มีทางทำให้ call ถูกยกเลิกได้
นั่นหยุดการฆ่า call ได้ แต่ไม่ได้แตะปัญหาลึกเลย: call ยังคงบล็อกอยู่ ชุดเทสต์สี่นาทียังคงกินเวลาเซสชันไปสี่นาทีเต็ม ๆ ที่โมเดลทำอะไรไม่ได้เลย — อ่านไฟล์ที่พังก็ไม่ได้ เตรียมการแก้ไขถัดไปก็ไม่ได้ คิดก็ไม่ได้ heartbeat ทำให้การรอเอาตัวรอดได้ แต่ไม่ได้ทำให้การรอมีประโยชน์
แก้ครั้งที่สอง: background job — และแฟล็กที่เราต้องลบทิ้ง
0.11.0 เปิดตัวการรันแบบ background: รันคำสั่งเป็น job รับ handle กลับมาทันที แล้วค่อยมาเก็บ output ทีหลัง แต่ละ job เป็น MCP resource ที่มี URI อย่าง octofs://jobs/17342-1 อ่านสถานะและ output ได้ตลอดเวลา
มันมาพร้อมแฟล็ก background บนเครื่องมือ shell และเมื่อมองย้อนกลับไป แฟล็กนั้นคือความผิดพลาดที่เราจำหน้าตาได้ดี ใน 0.9.0 เราลบสวิตช์ --line-mode ทิ้งเพราะความปลอดภัยที่ส่งมาหลังแฟล็กคือความปลอดภัยที่คนส่วนใหญ่ไม่เคยเปิดใช้ แฟล็ก background คือบั๊กเดียวกันที่ใส่เสื้อผ้าคนละชุด: มันขอให้โมเดลทำนายระยะเวลาของคำสั่งก่อนจะรันมัน โมเดลห่วยเรื่องนี้ในแบบที่คุณคาดเดาได้เป๊ะ ๆ — cargo build เสร็จทันทีบน cache ที่อุ่นอยู่ แต่กินหกนาทีถ้าเริ่มจากศูนย์ และแฟล็กก็เปลี่ยนข้อเท็จจริงที่ไม่มีทางรู้ล่วงหน้านั้นให้กลายเป็นการตัดสินใจที่ถูกบังคับให้ทำ เดาเป็น background กับคำสั่งที่เร็ว คุณก็ได้การสื่อสารไปกลับที่ไร้ประโยชน์เพิ่มมาหนึ่งรอบ เดาเป็น foreground กับคำสั่งที่ช้า คุณก็กลับไปเจอ call ที่บล็อกแบบเดียวกับที่เราเริ่มเรื่องมา
0.13.0 จึงลบแฟล็กทิ้ง แล้วแทนที่การทำนายด้วยการวัดจริง ทุกคำสั่งเริ่มต้นใน foreground ถ้ายังรันอยู่เมื่อครบสิบวินาที มันจะถูกเลื่อนขั้นเป็น background job โดยอัตโนมัติ — โพรเซสเดิม ไม่ถูกฆ่า ไม่ถูกรีสตาร์ต การเก็บ output คงทนตั้งแต่ไบต์แรก การข้ามเส้นตายจึงไม่ทำอะไรหายเลย: อะไรก็ตามที่คำสั่งพิมพ์ออกมาตอนยังอยู่ใน foreground จะนั่งรออยู่ในล็อกของ job ตอนคุณกลับมาอ่านทีหลัง
ตอนเลื่อนขั้น tool call จะรีเทิร์นทันที พร้อม resource link ที่พกคำสั่งไว้เป็นชื่อ — ไคลเอนต์จึงแสดงผล "make test … still running" ได้โดยไม่ต้องไปสืบใหม่ว่า job นั้นคืออะไร แม้จะผ่านการบีบอัดคอนเท็กซ์มาแล้วก็ตาม เมื่อโพรเซสจบการทำงาน octofs จะส่ง notifications/resources/updated สำหรับ URI ของ job นั้น ไคลเอนต์อ่าน resource ครั้งเดียวก็ได้ exit code และส่วนท้ายของ output ไม่มี polling ไม่มี call ที่เปิดค้าง ไม่มีโพรเซสกำพร้า
รายละเอียดสองอย่างในกลไกนั้นได้ที่นั่งของตัวเองมาด้วยบทเรียนราคาแพง:
- ส่วนท้าย ไม่ใช่ส่วนหัว การอ่าน resource คืน output ไม่เกิน 30 KB สุดท้าย ล็อกบิลด์นั้นยาว และคำตัดสิน — error, สรุปผลเทสต์ตอนจบ — อยู่ที่ท้ายเสมอ การป้อน 30 KB แรกของล็อกที่บรรทัดสุดท้ายเขียนว่า
FAILEDให้โมเดล คือสูตรสำเร็จของรายงานสุดมั่นใจว่าทุกอย่างผ่านหมด - เส้นทางส่งสองสาย ไคลเอนต์บน MCP revision 2026-07-28 ที่เปิดสตรีม subscription ไว้จะได้รับการแจ้งจบงานผ่านสตรีมนั้น ส่วนไคลเอนต์รุ่นเก่ากว่าจะได้รับ push แบบไม่ต้องร้องขอตามที่สเปกรุ่นก่อนอนุญาต และตั้งแต่ 0.13.0 ไคลเอนต์ที่ subscribe ช้า — หลังจาก job จบไปแล้ว — จะได้รับการแจ้งจบงานแบบ replay ให้ แทนที่จะต้องนั่งรอชั่วนิรันดร์กับ notification ที่ยิงออกไปก่อนจะมีใครฟังอยู่
หน้าต่าง foreground ยังหดจากสามสิบวินาทีเหลือสิบใน 0.13.0 ด้วย และนั่นคือ auto-promotion ที่จ่ายค่าตัวมันเอง: เมื่อการข้ามเส้นแบ่งไม่มีต้นทุนอะไรเลย — โพรเซสเดิม, output ที่คงทน, notification ตอนจบ — ก็ไม่มีเหตุผลจะจับเซสชันเป็นตัวประกันครึ่งนาที เผื่อว่าคำสั่งอาจจะเสร็จพอดีตอนวินาทีที่ยี่สิบห้า
แก้ครั้งที่สาม: ให้ job รันเคียงข้างกันได้
0.11.0 ระมัดระวังตัวมาก: หนึ่ง job ต่อหนึ่งไดเรกทอรี จบแค่นั้น ปลอดภัยก็จริง แต่ทื่อเกินไป — มันบังคับให้บิลด์กับการ tail ล็อกที่ไม่มีธุระอะไรต้องรอกันเลย ต้องเข้าคิวต่อกัน
0.14.0 บีบการ์ดให้แคบลงเหลือกรณีเดียวที่เป็นบั๊กจริง ๆ: คำสั่งเดียวกันเป๊ะที่กำลังรันอยู่แล้วในไดเรกทอรีเดียวกัน นั่นไม่ใช่ concurrency นั่นคือชุดเทสต์ที่ถูกยิงซ้ำสองรอบจากเรื่องเปิดบทความ และแทนที่จะปล่อยให้แข่งกัน octofs จะปฏิเสธมันแล้วบอกโมเดลชัด ๆ ว่าต้องทำอะไร:
The same shell command is already running as background job
octofs://jobs/17342-1 (`cargo test`). Wait for its completion — you will
get a resources/updated notification with its output — instead of
starting a duplicate. Independent commands may run concurrently in this
directory.
คำสั่งที่ต่างกันรันเคียงข้างกันได้ ส่วนตัวที่ซ้ำจะได้ error ที่ — อีกครั้ง — เป็นคำสั่งวิธีกู้คืนในตัวมันเอง
และกฎเหล็กหนึ่งข้อรองรับทั้งหมดนี้
เมื่อเซิร์ฟเวอร์เป็นฝ่ายรอให้แล้ว การที่โมเดลเผา tool call ไปกับ sleep 240 ก็เลิกเป็นทางลัดอันชาญฉลาด และกลายเป็นความสูญเปล่าล้วน ๆ 0.10.4 จึงเพิ่มมันเข้าไปในรายการการใช้ shell ผิดวิธี เคียงข้าง watch และ top:
Waiting with a bare `sleep` is forbidden — it burns the whole tool call
doing nothing.
To wait for a condition, poll it in a loop (sleep inside a loop body
is allowed):
until <check>; do sleep 2; done
To wait for a command you started, run it normally; long-running
commands automatically move to the background and notify you when
they finish.
นโยบายเดียวกับการปฏิเสธ grep ใน 0.9.0: อย่าใบ้ จงล้มเหลว — แล้วใส่ท่าที่ถูกต้องลงไปใน error sleep เปล่า ๆ octofs จะปฏิเสธ ส่วน sleep ในลูป until คือการ poll เงื่อนไขที่ชอบธรรมและผ่านได้ watch กับ top มันปฏิเสธเพราะไม่มีวันจบการทำงาน ซึ่งใน shell แบบ event-driven แปลว่ามันจะถือช่องเลื่อนขั้นไว้ตลอดกาล และส่ง notification แจ้งจบงานในวันที่ไม่มีวันมาถึง
เส้นเรื่องที่ไหลอยู่ข้างใต้: เหลือที่ให้หลอนน้อยลง
รอบ ๆ งานฝั่ง shell มีรีลีสเล็กอีกห้าตัวที่สาวต่อเส้นด้ายจาก 0.9.0 — ปิดช่องที่โมเดลอาจเข้าใจผิดว่าความเงียบหรือความกำกวมคือข้อมูล
ผลการค้นหาที่ว่างเปล่าบอกออกมาตรง ๆ การค้นหาที่ไม่เจออะไรจะคืนแค่... ความว่างเปล่า ก็ย่อมได้ — และโมเดลที่ได้รับสตริงว่างไม่ได้สรุปว่า "ไม่มีอะไรตรง" อย่างน่าเชื่อถือเสมอไป บางครั้งมันสรุปว่า "เครื่องมือพัง" แล้วลองใหม่ บางครั้งที่แย่กว่านั้น มันเติมความเงียบด้วยสิ่งที่มันคาดว่าจะเจอ แล้วเดินหน้าต่อราวกับเจอมาแล้วจริง ๆ กรณีไม่พบผลลัพธ์จึงเป็นประโยคที่ระบุว่าค้นหาอะไรไปและมีศูนย์รายการที่ตรงกัน — พฤติกรรมที่มีมาตั้งแต่ 0.7 และ 0.10.2 ตรึงหมุดไว้ด้วยเทสต์เพื่อไม่ให้มันถดถอยไปเงียบ ๆ ได้ การไม่มีหลักฐาน ถูกแถลงออกมาเป็นหลักฐานของการไม่มี
สคีมาของเครื่องมือเลิกมีตัวเลือก null (0.10.3) optional แบบ nullable ใน JSON schema อ่านแล้วดูปกติดีสำหรับมนุษย์ แต่เป็นกับดักล่อใจสำหรับโมเดล — "path": null คือ call ที่ validate ผ่านแล้วไม่พาไปที่ดีเลยสักที่ ตอนนี้ optional แปลว่าไม่ต้องส่งมา
line ID ที่ล้าสมัยรายงานได้ดีขึ้น (0.10.5) ข้อความผิดพลาดจากการตรวจสอบใน 0.9.0 — ตัวที่แสดงเนื้อหาสด ๆ พร้อมบอกว่าเป้าหมายของคุณย้ายไปไหน — แม่นยำขึ้นทั้งสองเรื่อง
การลิสต์ไฟล์และการค้นหาเร็วขึ้น (0.10.1) — นับบรรทัดใหม่บน raw byte แทนการแปลง UTF-8 ที่สูญเสียข้อมูล เอาชนิดไฟล์จาก directory walker แทนการ stat ซ้ำซ้อนทีละรายการ และ prefilter ทั้งบัฟเฟอร์ที่ข้ามไฟล์ที่ไม่ตรงก่อนจะลงมือทำงานระดับบรรทัด latency ในเครื่องมือที่โมเดลเรียกหลายร้อยครั้งต่อเซสชันคือภาษีที่เก็บจากทุกสิ่ง
เครื่องรีโมต แบบไม่ต้องมีพิธีรีตอง
Octofs พูด SSH/SFTP ได้ตั้งแต่ 0.8.0 — ชี้เครื่องมือไปที่ ssh://user@host/path แล้วเอเจนต์ก็ได้ระบบไฟล์ที่ตรวจสอบได้แบบเดียวกันบนเครื่องรีโมต สองรีลีสนี้มาปิดงานให้จบ
0.14.0 แปลงเป้าหมายผ่าน ~/.ssh/config ทั้ง Host alias, bastion แบบ ProxyJump, IdentityFile, IdentityAgent, ผู้ใช้และพอร์ตแบบรายโฮสต์ — คอนฟิกที่คุณเขียนไว้ให้นิ้วของตัวเองใช้ ตอนนี้มีผลกับการเชื่อมต่อของเอเจนต์ด้วย บททดสอบง่ายมาก: ถ้า ssh box เฉย ๆ ใช้ได้ในเทอร์มินัลของคุณ ssh://box/path ก็ใช้ได้ใน octofs รวม bastion และทุกอย่าง (ขอพูดตรง ๆ ว่าฮอปเดียวเท่านั้น: สาย ProxyJump หลายฮอปและ ProxyCommand เราปฏิเสธพร้อม error ที่ชัดเจน แทนที่จะรองรับแบบครึ่ง ๆ กลาง ๆ) ก่อนหน้านี้เอเจนต์ต้องการเป้าหมายที่สะกดครบทุกตัวอักษร ซึ่งเป็นสิ่งที่ไฟล์คอนฟิกของคุณเกิดมาเพื่อช่วยให้คุณไม่ต้องพิมพ์มันเองอยู่แล้ว
0.14.1 ทำให้การตรวจจับการใช้ผิดวิธีอ่านเข้าไปในคำสั่ง ssh ได้ เรื่องการปฏิเสธ grep จาก 0.9.0 มีรูโหว่ทรงรีโมตอยู่: ssh box 'grep -r TODO src/' ลอยผ่านตัวตรวจจับที่เคารพเครื่องหมายคำพูดสุภาพเกินกว่าจะมองเข้าไปข้างใน ตอนนี้มัน parse คำสั่งฝั่งรีโมตทะลุ option และการซ้อนของ SSH แล้วใช้กฎชุดเดียวกัน — view path="ssh://box/src" content="TODO" ตัวเดียวกับที่แทน grep ในเครื่อง ก็แทน grep ฝั่งรีโมตด้วย ไปป์ไลน์ยังอนุญาตเหมือนเดิม SSH แบบ interactive ยังไม่ถูกแตะต้อง
ฝั่งไคลเอนต์ของการจับมือ
ทุกอย่างข้างบนคือครึ่งเดียวของบทสนทนาระดับโปรโตคอล อีกครึ่งคือสิ่งที่รันไทม์เอเจนต์ของคุณทำกับ resource link และ notification resources/updated — และถ้ามันไม่ทำอะไรเลย background job ก็จะเสื่อมถอยกลับไปเป็น polling อย่างเงียบ ๆ
เอเจนต์ของเรา Octomind สร้างครึ่งของตัวเองขึ้นในช่วงสัปดาห์เดียวกัน: background shell job ถูกติดตามด้วย lifecycle จริง รอดผ่านการบีบอัดคอนเท็กซ์ (นั่นแหละคือประโยชน์ของ resource link ที่พกชื่อคำสั่งมาด้วย) และการแจ้งจบงานลงจอดในเซสชันทันทีที่ notification มาถึง งานชิ้นนั้นกินเวลาข้ามรอบ 0.47–0.48 และถูกเล่าไว้ในบทความรีลีส Octomind 0.48.0 ที่เผยแพร่สัปดาห์นี้ — พร้อมกับส่วนที่เหลือของรีลีสที่ลบโค้ดโปรดักชันสุทธิไปเกือบหนึ่งหมื่นบรรทัด ถ้าอยากเห็นว่าเอเจนต์หน้าตาเป็นยังไงเมื่อ shell เลิกบล็อกมัน: มันเริ่มบิลด์ แก้ไฟล์ถัดไประหว่างที่บิลด์รัน แล้วอ่านคำตัดสินเมื่อคำตัดสินมีอยู่จริง
แต่ไม่มีอะไรใน octofs ที่พึ่งพา Octomind เลย ทั้ง job resource, link และ notification ล้วนเป็น MCP เพียว ๆ — ไคลเอนต์ไหนก็ตามที่ทำตามโปรโตคอลจะได้ shell แบบ event-driven ไปฟรี ๆ
อัปเกรด
# Homebrew
brew upgrade muvon/tap/octofs
# Cargo
cargo install octofs --version 0.14.1
# npm
npm install -g @muvon/octofs
ไบนารีสำเร็จรูปสำหรับ Linux, macOS และ Windows (x86_64 และ ARM64) อยู่ที่หน้า releases
ไม่ต้องแก้คอนฟิกอะไร มีเรื่องพฤติกรรมหนึ่งข้อที่ควรรู้: ถ้า prompt หรือโค้ดไคลเอนต์ของคุณส่งแฟล็ก background ให้เครื่องมือ shell ให้ลบออก — แฟล็กหายไปแล้วและการเลื่อนขั้นเป็นอัตโนมัติ เหมือนกับสวิตช์เลือกโหมดที่เราลบทิ้งใน 0.9.0 ไม่มีอะไรมาแทนที่ เพราะพฤติกรรมที่ถูกต้องกลายเป็นพฤติกรรมเดียวที่มีอยู่แล้ว
ความต่างจะปรากฏตั้งแต่คำสั่งแรกที่รันเกินสิบวินาที แทนที่จะได้เซสชันที่ถูกบล็อก call ที่ถูกฆ่า หรือการรันซ้ำสอง เอเจนต์ของคุณจะได้เทิร์นของตัวเองคืนมา ได้ลิงก์ไปยัง job ที่กำลังรัน และได้ notification เมื่อมีอะไรที่คุ้มค่าจะอ่าน
Octofs เป็นโอเพนซอร์ส (Apache 2.0) ที่ github.com/Muvon/octofs บทความ 0.9.0 ว่าด้วยเหตุผลที่เลขบรรทัดเชื่อถือไม่ได้ ส่วนบทความนี้ว่าด้วยเหตุผลที่ tool call ที่ถูกบล็อกก็เชื่อถือไม่ได้เช่นกัน หลักการเดียวกันทั้งสองครั้ง — หน้าที่ของเซิร์ฟเวอร์คือยื่นสิ่งที่โมเดลลงมือทำต่อได้ให้กับมัน และ "รอตรงนี้ระหว่างที่ไม่มีอะไรเกิดขึ้น" ไม่เคยเป็นสิ่งนั้นเลย



