RFC-0010: which states may reach FAILED + ประกาศ transition ลง manifest (#14) - #16
Conversation
PR #13 ส่ง states.FAILABLE ขึ้น main พร้อมหมายเหตุว่ายังต้องยืนยันด้วย RFC ซึ่ง CONTRIBUTING.md บังคับสำหรับการเปลี่ยน lifecycle — RFC-0010 ยืนยัน สิ่งที่โค้ดเลือกไว้แล้วโดยไม่แก้พฤติกรรม พร้อมกันนี้ contract-semantics.yaml ประกาศ job_state_machine ให้ agent-platform อ้างอิงได้ แต่มีแค่ states/terminal/invariants/layering ไม่มี transition เลยสักอย่าง ทั้งที่ states.py ใช้สี่อย่างสร้างตาราง สามในสี่มี RFC-0001/0007 รองรับอยู่แล้ว ขาดแค่ยังไม่ได้บันทึก จึงลงให้ครบ รอบเดียว: progression, awaiting_from, failable, timeoutable, cancellable RFC บันทึกสองข้อที่การ implement ทำให้เห็นและยังไม่ตัดสินในนี้ - GOVERNANCE_ANALYSIS ที่ล้มแบบ error ลงเอยที่ TIMED_OUT — ยอมรับโดยตั้งใจ เพื่อให้ FAILED คงความหมายว่า "งานที่อนุมัติแล้วไม่สำเร็จ" - APPROVED ไม่อยู่ทั้งใน FAILABLE และ TIMEOUTABLE จึงค้างได้โดยไม่มี ทางออกอัตโนมัติ — เสนอให้เพิ่มเข้า TIMEOUTABLE ใน issue แยก ไม่ bump semantics_version เพราะ frozen scope ไม่ถูกแตะ และการ bump จะทำให้ drift check ของ agent-platform แดงทันทีจนกว่าจะ re-pin ยืนยันแล้วว่า manifest ตรงกับ states.py ทุกชุด · pytest 302 passed Closes #14 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
จากทีม ✅ ตรวจแล้ว ไม่กระทบฝั่งเรา ไม่ต้องทำอะไร
ให้มากกว่าที่ขอเราขอแค่ จุดที่มีค่าที่สุดคือคำเตือนใต้
ถ้าไม่มีบรรทัดนี้ คนที่เขียน validator จากตารางนี้จะได้ตรรกะที่ผิดแบบเงียบ ๆ — เหตุผลของ Decision 1 ก็ตรงกับที่เรา comment ไว้ที่ #14 — การปล่อยให้ ข้อสังเกตเล็ก ๆ ข้อเดียว —
|
Decision 2 — ยืนตามเดิมว่า GOVERNANCE_ANALYSIS ที่ error ลงเอยที่ TIMED_OUT แต่เพิ่มข้อกำหนดว่า reason ต้องแยก sla_exceeded / analysis_error ให้ได้ ไม่งั้น audit อ่านไม่ออกว่าช้าหรือพัง · และย้ำว่า retry เป็นของ orchestration ตาม RFC-0004 + RFC-0007 D1 — TIMED_OUT จึงเป็นคำตอบที่ถูกด้วยความหมาย ไม่ใช่เพราะเหลือตัวเลือกเดียว Open Question APPROVED — เคาะแล้วว่าควรเพิ่มเข้า TIMEOUTABLE เหตุผลหลักคือ การอนุมัติต้องมีวันหมดอายุ ซึ่งตรงกับที่ D1 ห้ามชุบชีวิต job ที่ FAILED (stale APPROVED) · เป็นการเปลี่ยนพฤติกรรมและเป็นกฎของ RFC-0007 จึงแยกไปทำที่ #17 ไม่ทำใน PR นี้ cancellable เปลี่ยนจาก prose เป็น list ให้ parse ได้เหมือน failable/timeoutable ตามที่ทีม agent-platform ติงไว้ · ตรวจแล้วว่าตรงกับ states ลบ terminal pytest 302 passed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ตอบสองข้อที่ค้าง — เคาะแล้วอ่าน ข้อ 1 · Decision 2 — ยืนตามที่เสนอ (
|
| state | ทางออกทั้งหมด | ใครเป็นเจ้าของการเดินต่อ |
|---|---|---|
DRAFT |
GOVERNANCE_ANALYSIS · CANCELLED |
คน (ผู้ยื่น) — ค้างได้ ไม่ใช่ความผิดปกติ |
REJECTED |
DRAFT · CANCELLED |
คน (ผู้แก้) — เช่นกัน |
APPROVED |
TASK_PLANNING · CANCELLED |
ระบบ — แต่ไม่มีทางออกอัตโนมัติเลย |
DEPLOYABLE |
COMPLETED · AWAITING_APPROVAL · FAILED · CANCELLED |
ระบบ — มี FAILED รองรับอยู่ |
APPROVED เป็น state เดียวในทั้งเครื่อง ที่ระบบเป็นเจ้าของการเดินต่อ แต่ถ้าระบบไม่เดิน
ก็ไม่มีอะไรพามันออกจากตรงนั้นได้เลยนอกจากคนมากด cancel — และไม่มีใครเฝ้า APPROVED
เพราะหน้าตามันเหมือนความสำเร็จ
แต่ไม่ควรทำใน PR นี้ — PR #16 ประกาศตัวชัดว่าไม่เปลี่ยนพฤติกรรม การเพิ่ม APPROVED
เข้า TIMEOUTABLE เป็นการเปลี่ยนตารางจริง ควรเป็น issue แยก + amendment ของ RFC-0007
(เจ้าของกฎ TIMEOUTABLE) ไม่ใช่ RFC-0010 ซึ่งเป็นเรื่อง FAILED
ทำลงไปแล้วใน fb4d2d9
| ทำอะไร | ที่ไหน |
|---|---|
Decision 2 + ข้อกำหนดว่า reason ต้องแยก sla_exceeded / analysis_error และย้ำว่า retry เป็นของ orchestration |
rfcs/0010-failable-states.md |
Open Question APPROVED → Decided พร้อมเหตุผลเรื่อง approval หมดอายุ |
เดิม |
cancellable prose → list ตามที่ agent-platform ติงไว้ |
contract-semantics.yaml |
pytest 302 passed · ตรวจแล้วว่า cancellable ตรงกับ states ลบ terminal เป๊ะ
และอีกสามคีย์ยังตรงกับ states.py เหมือนเดิม
การเพิ่ม APPROVED เข้า TIMEOUTABLE ไม่ได้ทำใน PR นี้ — แยกไปที่ #17 เพราะเป็น
การเปลี่ยนพฤติกรรม และเป็นกฎของ RFC-0007 ไม่ใช่ RFC-0010
PR นี้พร้อม review/merge แล้วครับ 🙏
…) (#20) RFC-0010 decided this and deliberately left it unimplemented: TIMEOUTABLE is RFC-0007's rule, so the amendment belongs there. RFC-0007 Amendment 1 adds APPROVED to the "entered from" list for TIMED_OUT, and states.py follows. The argument is governance, not liveness. A job that can sit in APPROVED forever may start executing a week later under a verdict formed in a context that no longer holds — the same stale APPROVED that RFC-0007 Decision 1 refuses when it keeps FAILED terminal. That door was shut on one side and left open on the other. It is also a conformance gap rather than a proposal. approval/v1 has carried expires_at since it was written and says what it means: "approval ที่หมดอายุแล้ว ใช้เดินงานไม่ได้ ต้องขอใหม่ · งานที่ค้างรออนุมัติจนเลยกำหนดควรเข้าสถานะ timeout ไม่ใช่รอตลอดไป". So: * Decision carries expires_at and renders it under the contract's own name; approve()/decide() take it. Optional there and optional here — an approval with no deadline never expires, and existing jobs are untouched. * The engine refuses to enter any POST_APPROVAL state under a lapsed approval (ExpiredApproval), including the way back out of a pause. Both halves of the direction lock now guard the same set, so there is no state reachable without a valid approval but not without any approval. * Replay refuses a trail that records it happening anyway (ExecutionAfterExpiry). The deadline and the moment are both in the log, so the claim is checkable by a reader who was not there — the same reason UnauditedExecution exists. * conformance/payload_check.py validates a real approval payload carrying expires_at, and probes both refusals; simulation gains approval_expired, whose job settles at TIMED_OUT rather than at a failure. contract-semantics.yaml records timeoutable and the enforced field. No semantics_version bump: frozen is untouched, exactly as in #16. Not done, and reported instead of guessed: nothing here sets expires_at or fires a timeout — the policy values stay out of scope in RFC-0007 and RFC-0010 alike — and the lifecycle still has no edge for "ขอใหม่", so a job whose approval lapsed settles at TIMED_OUT and a re-request is a new job with nothing linking it back. Co-authored-by: monthop-gmail <monthop-gmail@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ปิด #14 · ไม่มีการเปลี่ยนพฤติกรรม — ยืนยันสิ่งที่
states.FAILABLEทำอยู่แล้ว แล้วเติมเอกสารกับ manifest ให้ตรงกัน ·
pytest302 passedทำไม
PR #13 ส่ง
states.FAILABLEขึ้นmainพร้อมหมายเหตุว่ายังต้องยืนยันด้วย RFC ซึ่งCONTRIBUTING.mdบังคับสำหรับการเปลี่ยน lifecycle → ตอนนี้บนmainมีการตัดสินใจเชิงสถาปัตยกรรมที่ไม่มี RFC รองรับ และ
contract-semantics.yamlที่agent-platformอ้างอิงได้ก็ไม่ได้ประกาศกฎนั้นRFC-0010 ตัดสินอะไร
Decision 1 —
FAILEDไปถึงได้จาก 5 state หลัง approval เท่านั้น(
TASK_PLANNINGIN_PROGRESSAWAITING_APPROVALVALIDATINGDEPLOYABLE)ยืนยันตามที่โค้ดเลือก ไม่แก้โค้ด · เหตุผลยืนได้เองโดยไม่ต้องอ้างว่า "โค้ดทำแบบนี้อยู่แล้ว":
ก่อน
APPROVEDไม่มี execution ให้ล้ม และการใช้FAILEDแทนจะทำให้REJECTED/CANCELLED/
TIMED_OUTยุบรวมกัน ซึ่งเป็นเหตุผลเดียวกับที่ RFC-0007 แยกสองตัวหลังออกมาตั้งแต่แรกDecision 2 —
GOVERNANCE_ANALYSISที่ล้มแบบ error (ไม่ใช่คำตัดสิน) ลงเอยที่TIMED_OUTเพราะ
REJECTED/CANCELLED/FAILEDใช้ไม่ได้ทั้งหมด · ยอมรับโดยตั้งใจ ต้นทุนคือ jobที่พังทันทีต้องรอ timeout แลกกับการที่
FAILEDคงความหมายว่า "งานที่อนุมัติแล้วไม่สำเร็จ"ซึ่งเป็นสิ่งที่ recovery ผ่าน
supersedes_job_idของ RFC-0007 ตั้งอยู่บนสมมติฐานนั้น— บันทึกไว้ตรง ๆ เพื่อไม่ให้คนอ่านรอบหน้าคิดว่าลืม
ช่องว่างใน manifest กว้างกว่าแค่
failablestates.pyใช้ 4 อย่าง สร้างตาราง manifest ไม่ประกาศไว้สักอย่าง — แต่ 3 ใน 4 มี RFC รองรับแล้วขาดแค่ยังไม่ได้บันทึก จึงลงให้ครบรอบเดียว ไม่งั้นปิด #14 แล้ว manifest ก็ยังไม่ครบ
states.py_PROGRESSIONprogressionTIMEOUTABLEtimeoutableCANCELLEDจากทุก non-terminalcancellableFAILABLEfailableบวก
awaiting_fromที่ประกาศเป็น กฎ ไม่ใช่ edge เพราะ resolve ต่อ job ตอน runtimeเขียนเป็นตาราง static ไม่ได้ — ถ้าปล่อย
progression.AWAITING_APPROVAL: []ไว้เฉย ๆconsumer จะสรุปว่าเป็น dead end ซึ่งผิด
ตรวจแล้วว่า manifest ตรงกับ
states.pyทุกชุด (failabletimeoutableterminalstatesprogressionเฉพาะ non-terminal) — สคริปต์ที่ใช้ตรวจอยู่ท้าย PRไม่ bump
semantics_version— และทำไมagent-platformเทียบderived_from.semantics_versionกับค่า ระดับไฟล์ ของ manifest นี้(
conformance/drift_check.py→check_derived) การ bump จะทำให้ drift check ของเขาแดงทันทีจนกว่าจะ re-pin ทุก contract — เพื่อการเปลี่ยนแปลงในบล็อกที่เขาไม่ได้ derive จากมันด้วยซ้ำ
frozenscope ไม่ถูกแตะ จึงไม่ bump · แต่ RFC ตั้งเป็น Open Question ไว้ว่าการเปลี่ยนใน
not_derivedไม่มีสัญญาณบอก consumer เลย ซึ่งเป็นช่องว่างจริงของกติกาปัจจุบันสองข้อที่ RFC ตั้งเป็น Open Question ไม่ได้ตัดสินใน PR นี้
APPROVEDค้างได้โดยไม่มีทางออกอัตโนมัติ — ไม่อยู่ทั้งในFAILABLEและTIMEOUTABLEทางออกมีแค่
TASK_PLANNINGกับCANCELLEDถ้า orchestration ตายหลังอนุมัติแต่ก่อนวางแผนjob จะค้างตลอดไปจนกว่าคนจะยกเลิก · เสนอให้เพิ่มเข้า
TIMEOUTABLEแต่ไม่ตัดสินที่นี่เพราะเป็นคำถามของ
TIMED_OUTไม่ใช่FAILEDและจะกลายเป็นการแก้โค้ด ขณะที่ทั้ง PR นี้เป็นการยืนยัน→ ขอเปิด issue แยก
not_derivedไม่มี version signal — ตามข้างบนไฟล์ที่แตะ
rfcs/0010-failable-states.mdcontract-semantics.yamlprogressionawaiting_fromfailabletimeoutablecancellable+ invariant + source pointerpackages/core/state-machine.mdFAILEDให้ครบ จากเดิมมีเส้นเดียวpackages/core/devfactory_core/job.py·states.py·packages/core/README.mdrfcs/0001-job-state-machine.mdFollow-up ที่แนะนำ
เพิ่ม conformance check ว่า manifest ยังตรงกับ
states.pyเพื่อกัน drift แบบเดียวกับที่เกิดระหว่างPR #13 กับ RFC นี้ — ตรรกะที่ผมใช้ตรวจ PR นี้ยกไปใช้ได้เลย:
✅ สองคำถามที่ค้าง — ตอบแล้ว 2026-08-19
TIMED_OUT) + เพิ่มข้อกำหนดว่าreasonต้องแยกsla_exceeded/analysis_error— บันทึกลง RFC แล้วในfb4d2d9APPROVED→TIMEOUTABLEเคาะแล้วว่าควรทำ แต่แยกไป lifecycle: APPROVED ควรเข้า TIMEOUTABLE — การอนุมัติที่ไม่มีวันหมดอายุ (แยกจาก #16) #17 เพราะเป็นการเปลี่ยนพฤติกรรมและเป็นกฎของ RFC-0007 ไม่ใช่ RFC-0010
เหตุผลเต็มอยู่ในคอมเมนต์นี้
·
cancellableแก้เป็น list แล้วตามที่agent-platformติงไว้Closes #14
🤖 Generated with Claude Code