# Octofs 0.14: การรอไม่ใช่ tool call

> สิบเอ็ดรีลีสของ octofs ในสองสัปดาห์เปลี่ยน shell จาก tool call ที่ช้าที่สุดและเปราะบางที่สุดให้กลายเป็นแบบ event-driven: ทุกคำสั่งเริ่มต้นใน foreground เลื่อนขั้นเป็น background job อัตโนมัติเมื่อครบสิบวินาทีโดยไม่เสียข้อมูลแม้แต่ไบต์เดียว และแจ้งเตือนไคลเอนต์เมื่อจบการทำงาน พร้อมด้วยการแปลงเป้าหมายผ่าน ~/.ssh/config สำหรับระบบไฟล์รีโมต การตรวจจับการใช้ผิดวิธีที่อ่านเข้าไปในคำสั่ง ssh และการค้นหาที่พูดออกมาดัง ๆ ว่า "ไม่พบอะไรเลย" โอเพนซอร์สภายใต้ Apache-2.0

# Octofs 0.14: การรอไม่ใช่ tool call

เอเจนต์รันชุดเทสต์ ชุดเทสต์ใช้เวลาสี่นาที ส่วน idle timeout ของ MCP client อยู่ที่หกสิบวินาที

คุณคงเดาออกว่าเรื่องนี้จะจบยังไง พอถึงวินาทีที่หกสิบ ไคลเอนต์ก็ยกเลิก call ทิ้ง โพรเซสยังรันต่อไป — ไม่มีใครสั่งให้มันหยุด — ขณะที่โมเดลซึ่งถือใบยกเลิกอยู่ในมือแทนที่จะเป็นผลเทสต์ ก็ทำสิ่งที่สมเหตุสมผลที่สุด: รันชุดเทสต์อีกรอบ ชุดเทสต์สองชุด ไดเรกทอรีเดียวกัน แข่งกันแย่ง build artifact ชุดเดียวกัน รอบที่สองล้มเหลวด้วย locking error โมเดลรายงานว่าเทสต์พัง ทั้งที่เทสต์ไม่ได้พังเลย

ในอีกเซสชันหนึ่ง โมเดลตัวเดิมที่เคยเจ็บมาก่อน ก็พัฒนาทางลัดของตัวเองขึ้นมา: รันบิลด์ แล้วเรียก `sleep 240` แล้วค่อยไปดู tool call ที่ไม่ทำอะไรเลย ถูกเปิดค้างไว้สี่นาที เพียงเพื่อให้ tool call อีกตัวอาจมีอะไรให้ดูบ้าง โมเดลได้ประดิษฐ์ polling ขึ้นมาใหม่แบบห่วย ๆ เพราะเราไม่เคยยื่นอะไรที่ดีกว่านั้นให้มันเลย

[คราวก่อนที่เราเขียนถึง octofs](/blog/octofs-0-9-0-content-verified-line-ids) หัวข้อคือการแก้ไขที่ลงผิดบรรทัด บทความนั้นปิดท้ายด้วยหลักการหนึ่ง: อินเทอร์เฟซที่แท้จริงของ 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](https://octomind.run) สร้างครึ่งของตัวเองขึ้นในช่วงสัปดาห์เดียวกัน: background shell job ถูกติดตามด้วย lifecycle จริง รอดผ่านการบีบอัดคอนเท็กซ์ (นั่นแหละคือประโยชน์ของ resource link ที่พกชื่อคำสั่งมาด้วย) และการแจ้งจบงานลงจอดในเซสชันทันทีที่ notification มาถึง งานชิ้นนั้นกินเวลาข้ามรอบ 0.47–0.48 และถูกเล่าไว้ใน[บทความรีลีส Octomind 0.48.0](https://octomind.run/blog/octomind-0-48-0-release) ที่เผยแพร่สัปดาห์นี้ — พร้อมกับส่วนที่เหลือของรีลีสที่ลบโค้ดโปรดักชันสุทธิไปเกือบหนึ่งหมื่นบรรทัด ถ้าอยากเห็นว่าเอเจนต์หน้าตาเป็นยังไงเมื่อ shell เลิกบล็อกมัน: มันเริ่มบิลด์ แก้ไฟล์ถัดไประหว่างที่บิลด์รัน แล้วอ่านคำตัดสินเมื่อคำตัดสินมีอยู่จริง

แต่ไม่มีอะไรใน octofs ที่พึ่งพา Octomind เลย ทั้ง job resource, link และ notification ล้วนเป็น MCP เพียว ๆ — ไคลเอนต์ไหนก็ตามที่ทำตามโปรโตคอลจะได้ shell แบบ event-driven ไปฟรี ๆ

---

## อัปเกรด

```bash
# 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](https://github.com/muvon/octofs/releases)

ไม่ต้องแก้คอนฟิกอะไร มีเรื่องพฤติกรรมหนึ่งข้อที่ควรรู้: ถ้า prompt หรือโค้ดไคลเอนต์ของคุณส่งแฟล็ก `background` ให้เครื่องมือ shell ให้ลบออก — แฟล็กหายไปแล้วและการเลื่อนขั้นเป็นอัตโนมัติ เหมือนกับสวิตช์เลือกโหมดที่เราลบทิ้งใน 0.9.0 ไม่มีอะไรมาแทนที่ เพราะพฤติกรรมที่ถูกต้องกลายเป็นพฤติกรรมเดียวที่มีอยู่แล้ว

ความต่างจะปรากฏตั้งแต่คำสั่งแรกที่รันเกินสิบวินาที แทนที่จะได้เซสชันที่ถูกบล็อก call ที่ถูกฆ่า หรือการรันซ้ำสอง เอเจนต์ของคุณจะได้เทิร์นของตัวเองคืนมา ได้ลิงก์ไปยัง job ที่กำลังรัน และได้ notification เมื่อมีอะไรที่คุ้มค่าจะอ่าน

---

Octofs เป็นโอเพนซอร์ส (Apache 2.0) ที่ [github.com/Muvon/octofs](https://github.com/Muvon/octofs) [บทความ 0.9.0](/blog/octofs-0-9-0-content-verified-line-ids) ว่าด้วยเหตุผลที่เลขบรรทัดเชื่อถือไม่ได้ ส่วนบทความนี้ว่าด้วยเหตุผลที่ tool call ที่ถูกบล็อกก็เชื่อถือไม่ได้เช่นกัน หลักการเดียวกันทั้งสองครั้ง — หน้าที่ของเซิร์ฟเวอร์คือยื่นสิ่งที่โมเดลลงมือทำต่อได้ให้กับมัน และ "รอตรงนี้ระหว่างที่ไม่มีอะไรเกิดขึ้น" ไม่เคยเป็นสิ่งนั้นเลย
