ข้ามไปยังเนื้อหา

การทำงานร่วมกันแบบหลายคน — ตัวตน สมาชิกภาพ และสถานะออนไลน์

Control Center ทำงานได้กับผู้ปฏิบัติงานคนเดียว แต่โมเดลภายใต้มันเป็นแบบหลาย ผู้กระทำมาตั้งแต่แรก มนุษย์และเอเจนต์ส่งข้อความในสเปซเดียวกัน แก้ตั๋วงานใบเดียวกัน และปรากฏในรายชื่อเดียวกัน การทำให้สิ่งนี้ปลอดภัยและ เข้าใจได้สรุปอยู่ที่การตัดสินใจสามข้อ: ใครนับเป็นผู้กระทำ, อะไรกันการเข้าถึง และ สถานะใดถูกใช้ร่วมกันอย่างคงทน เทียบกับที่แค่ กระจายแบบสด

Principal: ผู้ใช้และเอเจนต์เป็นผู้กระทำที่เท่าเทียมกัน

หัวข้อที่มีชื่อว่า “Principal: ผู้ใช้และเอเจนต์เป็นผู้กระทำที่เท่าเทียมกัน”

ทุกอย่างที่บันทึกว่า “ใครทำสิ่งนี้” — ข้อความ ตั๋วงาน รีวิว แผน บันทึกการรัน รายการตรวจสอบ — ล้วนผ่าน Principal ซึ่งเป็นยูเนียนแบบปิดของ UserPrincipal และ AgentPrincipal ที่มีรูปแบบบนไวร์กะทัดรัด (user:<id> / agent:<id>) การระบุที่มาไม่เคยยุบ N มนุษย์ (หรือ N เอเจนต์) ให้เป็นบัญชีตัวแทนบัญชีเดียว รีวิวที่เอเจนต์ทิ้งไว้กับรีวิวที่คนทิ้งไว้จึงแยก ออกจากกันได้ทุกที่ที่ปรากฏ

เพราะ Principal เป็นหน่วยของตัวตน มันจึงเป็นหน่วยของการจำกัดขอบเขตด้วย:

  • ขีดจำกัดอัตราคิดต่อ Principal ไม่ใช่ต่อเซสชัน ตัวกันการถล่มสำหรับการ เรียกเครื่องมือระยะไกลคิดงบตามผู้ใช้ ไม่ใช่ตามการเชื่อมต่อ อุปกรณ์ทุกเครื่อง ของสมาชิกคนหนึ่งจึงดึงจากหน้าต่างเลื่อนหน้าต่างเดียวที่ใช้ร่วมกัน (120 ครั้ง ต่อนาที โดยเปลี่ยนแปลงสถานะได้ไม่เกิน 30 ครั้ง) การเปิดสามอุปกรณ์จึงไม่ทำ ให้โควตาของใครเพิ่มเป็นสามเท่า และสมาชิกที่ส่งถี่เกินไปคนเดียวก็อดอาหาร คนอื่นไม่ได้
  • การเลิกทำคิดต่อ Principal บันทึกการกระทำของไคลเอนต์แต่ละตัวบันทึกเฉพาะ การเปลี่ยนแปลงที่ Principal ของมัน ทำ คุณกด ⌘Z แก้ไขตั๋วงานของตัวเองได้ แต่เลิกทำของเพื่อนร่วมทีมไม่ได้ — กว่าคุณจะเห็นมัน เลนคงทนได้ตัดสินมันไปแล้ว (ดูด้านล่าง)

สมาชิกภาพคือขอบเขตการเข้าถึง — ไม่ใช่คีย์จับคู่

หัวข้อที่มีชื่อว่า “สมาชิกภาพคือขอบเขตการเข้าถึง — ไม่ใช่คีย์จับคู่”

ข้อมูลรับรองสองชิ้นที่แยกกันตอบคำถามสองข้อที่แยกกัน คีย์จับคู่ (ดู ควบคุมระยะไกลและมือถือ) ยืนยันตัวตนของ_อุปกรณ์_กับเซิร์ฟเวอร์ สมาชิกภาพ ให้สิทธิ์ผู้ใช้ของอุปกรณ์ นั้นลงมือทำภายใน_เวิร์กสเปซ_ อุปกรณ์ที่จับคู่แล้วแต่ผู้ใช้ไม่ถือสมาชิกภาพใน เวิร์กสเปซใด จะอ่านหรือแก้เวิร์กสเปซนั้นไม่ได้ทั้งคู่

สมาชิกภาพคือหนึ่งแถวต่อ (เวิร์กสเปซ, ผู้ใช้) เก็บไว้ในไฟล์ฐานข้อมูลของ เวิร์กสเปซนั้นเอง — คำถามว่า “ใครอยู่ในเวิร์กสเปซนี้?” จึงเป็นการอ่านแบบจำกัด ขอบเขตธรรมดา ขณะที่คำถามกลับกัน “ฉันอยู่ในเวิร์กสเปซใดบ้าง?” จำเป็นต้องข้าม ไฟล์ และเป็นการกระจายข้ามเวิร์กสเปซเพียงไม่กี่แห่งที่ได้รับอนุญาต มันคือสิ่งที่ ตัวเลือกเวิร์กสเปซรันก่อนจะเลือกเวิร์กสเปซใดได้

แต่ละแถวมีบทบาทจากบันไดห้าระดับที่เรียงตามลำดับชั้น:

บทบาท ทำอะไรได้
owner ควบคุมเต็มรูปแบบ รวมถึงลบเวิร์กสเปซ; มีได้เพียงหนึ่งเดียวต่อเวิร์กสเปซ
admin จัดการสมาชิก คำเชิญ การตั้งค่า และการดำเนินการที่ทำลายได้
member อ่าน-เขียนในชีวิตประจำวัน: ข้อความ ตั๋วงาน การรันเอเจนต์
viewer อ่านอย่างเดียวทั่วเวิร์กสเปซ; glob ยกเว้นความลับซ่อนพาธที่ตรงกันบนพื้นผิวที่มีโค้ด
guest พื้นการอ่านอย่างเดียวเหมือน viewer แต่อยู่ในขั้นต่ำสุดของบันได

การตัดสินใจเข้าถึงทุกครั้งลดรูปเป็น “บทบาทของผู้เรียกอย่างน้อยคือ X หรือไม่” พื้นขั้นต่ำได้มาจากชนิดของการดำเนินการ เว้นแต่การดำเนินการนั้นประกาศไว้เองอย่าง ชัดเจน: การอ่านต้องใช้ guest การเปลี่ยนแปลงต้องใช้ member การดำเนินการที่ ทำลายได้ต้องใช้ admin ACL ระดับวัตถุที่เกินไปกว่าสิทธิ์ระดับรีโปด้านล่างนี้ อยู่นอกขอบเขตโดยเจตนา

คำเชิญคือวิธีมอบสมาชิกภาพ จาก การตั้งค่า → เวิร์กสเปซ → สมาชิก แอดมินออก รหัสใช้ครั้งเดียว (เป็นลิงก์หรือ QR) ที่หมดอายุในเจ็ดวันโดยค่าเริ่มต้น เซิร์ฟเวอร์ เก็บเฉพาะแฮช SHA-256 ของรหัส และตัวรหัสเองแสดงให้เห็นเพียงครั้งเดียว การแลก รหัสจะจัดสรรผู้ใช้แบบ JIT บันทึกสมาชิกภาพตามบทบาทของคำเชิญ (ไม่เคยเป็น owner — ความเป็นเจ้าของถูกโอนอย่างชัดเจน ไม่เคยให้ผ่านคำเชิญ) แล้วส่งต่อไปสู่ การจับคู่อุปกรณ์ ข้อมูลรับรองของอุปกรณ์ที่ออกหลังการจับคู่ก็มีอายุจำกัด 30 วัน เช่นกัน การจับคู่จึงไม่ใช่คีย์ถาวร

สิทธิ์ระดับรีโปอยู่ใต้สมาชิกภาพ เพราะเซิร์ฟเวอร์ถือ checkout แบบเต็ม สมาชิกภาพในเวิร์กสเปซเพียงอย่างเดียวต้องไม่ให้สิทธิ์เกินโฮสต์โค้ดเด็ดขาด: พื้นผิวที่มีโค้ดทุกแห่ง — ไฟล์ ดิฟ รีวิว PR ค้นหาโค้ด — ตรวจสอบสิทธิ์ของสมาชิก บน_รีโปนั้น_ สิทธิ์เป็นราย (เวิร์กสเปซ, ผู้ใช้, รีโป) และเรียงเป็น none → read → review → write โดย write หมายถึงการสั่งรันเอเจนต์ที่เขียนลงรีโป การไม่มีแถวหมายถึง none เจ้าของและแอดมินถือ write โดยปริยายบนทุกรีโปที่ เชื่อมไว้ และไม่มีแถวเลย

Control Center แยกสถานะที่ใช้ร่วมกันออกเป็นสองเลนที่มีกฎความคงทนตรงข้ามกัน เพราะสถานะสองชนิดล้มเหลวคนละแบบ

สถานะออนไลน์ — ใครออนไลน์อยู่ อยู่ที่ไหน กำลังพิมพ์อะไร เอเจนต์กำลังทำอะไร อยู่ตอนนี้ — ไร้ค่าทันทีที่ล้าสมัย ถ้ามันถูกเก็บถาวร การเชื่อมต่อใหม่ทุกครั้ง จะต้องไกล่เกลี่ย “ฐานข้อมูลบอกว่า Sam กำลังแก้ตั๋วงานนี้” กับความจริง ดังนั้น สถานะออนไลน์จึงไม่เคยถูกเขียนลงฐานข้อมูล มันอยู่ในฮับสถานะออนไลน์ ที่เก็บในหน่วยความจำบนเซิร์ฟเวอร์: ไคลเอนต์เผยแพร่ด้วย presence.update เพื่อนร่วมงานสมัครรับด้วย presence.watch และรายการจะหมดอายุอัตโนมัติหลัง พลาดฮาร์ตบีตสามครั้ง (ฮาร์ตบีตวิ่งทุก 10 วินาที ฮับกวาดทุก 5) ไม่มีอะไร ให้ไกล่เกลี่ย — มีแต่ปล่อยให้ล่วงเลยไป

คุณสมบัติสองข้อทำให้เลนนี้น่าเชื่อถือ:

  • ตัวตนมาจากเซสชัน ไม่เคยมาจากอาร์กิวเมนต์ของไคลเอนต์ เซิร์ฟเวอร์แมป อุปกรณ์ที่ยืนยันตัวตนแล้วของการเชื่อต่อไปยัง id ผู้ใช้ ไคลเอนต์อัปเดตได้ เฉพาะสถานะออนไลน์ของตัวเองเท่านั้น ไม่มีใครปลอมสถานะหรือ locus ของ ผู้เข้าร่วมคนอื่นได้ การอัปเดต offline อย่างชัดเจน (ตัดการเชื่อมต่อหรือ สวิตช์ซ่อนตัว) จะลบรายการทันที
  • รายชื่อถูกฮับที่เซิร์ฟเวอร์ เพื่อนร่วมงานไม่แลกเปลี่ยนสถานะออนไลน์กัน โดยตรง ผู้เข้าร่วมทุกคนจึงเห็นรายชื่อเดียวกันไม่ว่าโครงข่ายจะเป็นอย่างไร เอเจนต์ไม่มีไคลเอนต์ เซิร์ฟเวอร์จึง_สังเคราะห์_รายการของเอเจนต์จากสถานะ การรันสด — กำลังคิด กำลังรัน ติดอยู่ที่เกตอนุมัติ เสร็จแล้ว — โดยมีรัน ตัวแทนเพียงหนึ่งเดียวเป็นตัวกำหนดสถานะและ locus ของเอเจนต์ เพื่อให้การรัน ที่ซ้อนกันไม่ทำให้รายชื่อกระพริบ

การกระจายแบ่งเป็นระดับตามผู้รับ: เดสก์ท็อปและเว็บอยู่ในระดับเต็ม (ส่งรายชื่อ ได้ไม่เกินหนึ่งครั้งต่อ 100 มิลลิวินาที) ส่วนโทรศัพท์อยู่ระดับสรุป (ต่อ 500 มิลลิวินาที) การอัปเดตความถี่ระดับเคอร์เซอร์จึงละลาย PWA ไม่ได้

locus ของผู้เข้าร่วม — ตำแหน่งที่เขาอยู่ตอนนี้ ละเอียดถึงชิ้นงาน — เป็น ยูเนียนแบบปิด: Space, File (รีโป + พาธ + เลขบรรทัดถ้ามี), Pr, Ticket, PlanNode ไม่ใช่ทุก locus ที่มีที่ให้กระโดดไป: ไม่มีเส้นทางไปยังตัวดูไฟล์ locus แบบ File จึงแสดงเป็นป้ายแทนที่จะเป็นปลายทาง เลนนี้ยังพาการอ้างสิทธิ์ แบบหลวมมาด้วย (“Sam กำลังแก้ตั๋วงานนี้”, “เอเจนต์ถือ worktree นี้อยู่”): เป็น_การมองเห็น_ความขัดแย้ง ไม่ใช่การล็อก ชนิด locus ที่ไม่รู้จักจะถูกแปลง เป็น null ไคลเอนต์เก่าจึงแค่ไม่แสดงสิ่งที่มันไม่เข้าใจ

ระเบียนที่ต้องอยู่รอด — ตั๋วงาน เอกสารบันทึกที่ใช้ร่วมกันของสเปซ ข้อความ — ไปอีกทางหนึ่ง: เป็นแหล่งอ้างอิงหลัก เรียงลำดับ และถูกเก็บถาวร การแก้ไขที่เกิดขึ้นพร้อมกัน ตัดสินด้วยผู้เขียนรายหลังสุดชนะตามลำดับที่เซิร์ฟเวอร์ได้รับ ไม่เคยใช้เวลา ของไคลเอนต์ การตัดสินใจตั้งใจให้ละเอียด: การแก้ตั๋วงานเป็น_รายคอลัมน์_ คนสองคน ที่แก้คนละฟิลด์ของตั๋วงานใบเดียวกันจึงลงได้ทั้งคู่และไม่มีใครทับอีกคน (การเปลี่ยนสถานะเวิร์กโฟลว์ยังใช้การล็อกแบบ optimistic แทน) ส่วนเอกสารบันทึก ของสเปซเป็นคอลัมน์เดียว เอกสารทั้งใบจึงเป็น LWW โดยการอ้างสิทธิ์แบบหลวมบน เลนสถานะออนไลน์ทำให้การแก้ไขพร้อมกันมองเห็นได้แทนที่จะถูกล็อก มันไม่ใช่ CRDT โดยเจตนา — ภาระการพิสูจน์อยู่ที่ CRDT เอง และ LWW ตามลำดับที่ได้รับบวกกับ การอ้างสิทธิ์ที่มองเห็นได้ก็ครอบคลุมรูปแบบการทำงานร่วมกันจริง ๆ ที่เกิดขึ้น

“ผู้เขียนรายหลังสุด” หมายถึงลำดับที่เซิร์ฟเวอร์ได้รับ ไม่เคยเป็นเวลาของ ไคลเอนต์ — แล็ปท็อปที่เวลาคลาดเคลื่อนชนะการแก้ไขที่มันแพ้ไม่ได้ ข้างหลัง เลนคงทนมีฟีดซิงค์: ทริกเกอร์ของ SQLite ต่อทุกการเปลี่ยนแปลงเข้าบันทึก การเปลี่ยนแปลงรายเวิร์กสเปซ_ภายในทรานแซกชันที่เขียน_ โดยจัดสรรหมายเลขลำดับ แบบโมโนโทนิกต่อเวิร์กสเปซในทรานแซกชันเดียวกับการเปลี่ยนแปลงนั้นเอง บริการ บนเซิร์ฟเวอร์ไล่อ่านบันทึกนั้นเป็นเฟรมเดลตาที่มีเวอร์ชันตามลำดับหมายเลข ไคลเอนต์พัฒนาจากสแนปชอตเต็มไปสู่เดลตา ช่องว่างในลำดับจะทำให้สโตร์นั้นถอยกลับ ไปใช้โหมดสแนปชอตแทนที่จะนำเฟรมไปใช้ผิด ๆ

เลนสถานะออนไลน์ไม่ใช่แค่รายชื่อ — มันขับเคลื่อนกริยาของการทำงานร่วมกัน:

  • โหมดติดตาม คลิกผู้เข้าร่วมคนหนึ่ง แล้วไคลเอนต์ของคุณจะตาม locus ของเขา เปลี่ยนหน้าไปขณะที่เขาเปลี่ยนหน้า การติดตามเป็นแบบ “จนกว่าคุณจะลงมือ”: ทันทีที่คุณเปลี่ยนหน้าด้วยตัวเอง การติดตามจะหลุดออก แบนเนอร์ที่ปิดได้จะ ประกาศเมื่อผู้เข้าร่วมคนอื่นกำลังสปอตไลต์ (นำเสนอ) สเปซที่คุณอยู่
  • ชี้นำ ข้อความถึงเอเจนต์ที่กำลังรันจะถูกเข้าคิวเป็นคำชี้นำ เอเจนต์จะอ่าน มันใหม่ก่อนรันเทิร์นถัดไป คุณจึงสะกิดการรันกลางทางได้โดยไม่ต้องฆ่ามัน
  • เข้าควบคุม การเข้ารับควบคุมการสนทนาจะหยุดการรันที่ทำงานอยู่ทุกรายการ ที่ขอบเทิร์นสะอาดถัดไป (การรันของ Claude Code ซึ่งไม่มีขอบเขตปลอดภัยจะถูก หยุด) โดยมีสแนปชอต git ต่อเทิร์นรองรับ การเข้าควบคุมจึงย้อนกลับได้ ตัวทำเครื่องหมายแบบคงทนจะบล็อกการสั่งงานใหม่เข้าสเปซ และอยู่รอดการรีสตาร์ต เซิร์ฟเวอร์ — เซิร์ฟเวอร์ไม่เคยกลับมาทำงานต่ออัตโนมัติทับการแก้ไขที่มนุษย์ ทำค้างไว้ การมีบรรณาธิการสดหนึ่งคนต่อ worktree เป็นการอ้างสิทธิ์แบบหลวม: การเข้าควบคุมครั้งที่สองจะมองเห็นได้และถูกปฏิเสธ ไม่ใช่ถูกรวมเงียบ ๆ
  • คืนงาน การคืนการควบคุมจะโพสต์สรุปดิฟแบบมีโครงสร้างของ worktree ลงสเปซ เข้าคิวเป็นคำชี้นำสำหรับการรันที่หยุดไว้ ทำการรันเหล่านั้นต่อ และล้าง เครื่องหมาย

การชี้นำและการเข้าควบคุมเป็นการแทรกแซงหลังเกิดเหตุ ปุ่มปรับความเป็นอิสระ คือการตั้งค่าถาวร: ต่อสเปซ ต่อเอเจนต์ ตั้งจากส่วนหัวของสเปซ มันตัดสินว่าเกิด อะไรขึ้นเมื่อเอเจนต์ไปถึงการกระทำที่มีเกต

ระดับ ค่าบนไวร์ ผลต่อการเรียกเครื่องมือที่มีเกต
เสนออย่างเดียว proposeOnly ถูกปฏิเสธทันที พร้อมเหตุผลที่เอเจนต์ได้รับคำสั่งให้ถ่ายทอด — อธิบายการกระทำได้แต่ทำไม่ได้
ทำเมื่อได้รับอนุมัติ actWithApproval เกตการอนุมัติแบบปิดเมื่อไม่ผ่าน: การยืนยันหนึ่งครั้งจะปรากฏต่อผู้ปฏิบัติงานที่เชื่อมต่ออยู่ และไม่มีผู้อนุมัติหมายถึงถูกปฏิเสธ
ทำได้อย่างอิสระ actFreely อนุมัติล่วงหน้าแล้ว ยกเว้นเมื่อมีกฎ deny แบบเด็ดขาดของกฎคุมใช้บังคับ — กฎ deny ชนะเสมอ
(ไม่ได้ตั้งค่า) null เหมือนทำเมื่อได้รับอนุมัติ: ค่าเริ่มต้นของเซิร์ฟเวอร์

มีสองอย่างที่อยู่นอกปุ่มนี้ทั้งสองทิศทาง กริยาเอาต์พุตของโหมดหนึ่งถูกปักหมุดไว้ และปฏิเสธไม่ได้ไม่ว่าปุ่มจะพูดว่าอะไร — โหมดประสานงานเคยไม่สามารถส่งมอบ สิ่งเดียวที่มันต้องส่งมอบได้เชิงโครงสร้าง เพราะพรีเซ็ตปฏิเสธคลาสเอฟเฟกต์ที่ กริยานั้นประกาศ และ deny แบบเด็ดขาดจากกฎคุมการกระทำก็ไม่อาจถูกแทนที่ด้วย actFreely

ทั้งหมดนี้เป็นการเพิ่มเติม ขั้นตอนการทำงานของผู้ปฏิบัติงานคนเดียวไม่เปลี่ยน: เมื่อมีมนุษย์หนึ่งคนและไม่มีเอเจนต์ ฮับสถานะออนไลน์ก็ไม่มีอะไรให้กระจาย เลนจึง ส่งสแนปชอตเริ่มต้นแล้วก็ว่างที่ทราฟฟิกศูนย์ กลไก LWW และการซิงค์ก็เป็น เส้นทางเขียนคนเดียวแบบเดิม และคีย์จับคู่ก็ยังคงเป็นข้อมูลรับรองอุปกรณ์อย่างที่ เคยเป็น สมาชิกภาพเป็นขอบเขตการเข้าถึงอยู่แล้ว — เมื่อมีมนุษย์คนเดียวก็มีแถว เจ้าของแถวเดียว และไม่มีส่วนติดต่อของรายชื่อ