Implement RFC-0012: JOB_SETTLED closes every terminal - #29
Merged
Conversation
Every job entering a terminal now emits JOB_SETTLED as its last record, carrying settled_as and event_count. COMPLETED emits it too, after JOB_COMPLETED, so the rule has no exception for a reader to have to know about. Replay gains UnsettledTrail, PrematureSettlement, and MiscountedTrail - the first end-of-trail checks it has had beyond the single COMPLETED case. Two things the implementation found, both corrected in the RFC rather than worked around. event_count is scoped by job_id, not by subject. GOVERNANCE_DECISION carries subject_type: approval and its own subject_id, so a subject-scoped count would have excluded exactly the records the count exists to notice, and replay_tenant groups by job_id anyway - that is where producer and reader count the same set. And the RFC's example for what the count adds was wrong: a missing GOVERNANCE_DECISION is already caught by UnauditedDecision. Driving it out properly, today's vocabulary has nothing whose absence only the count would catch - drop any record from a real trail and something else fires first. Its reach today is the extra direction, a forged or duplicated record that nothing demands, and it grows into the missing direction when TASK_ASSIGNED, EXECUTION_STARTED, and EXECUTION_FAILED start being emitted with no structural check of their own. There is a test asserting exactly that, so the claim stays honest as the vocabulary grows. The simulation had a test recording this hole as expected behaviour, predicting the fix would be a per-job sequence number in event/v1. It is now the regression test for the fix, and says the prediction was wrong and why - ADR-0015 established that a number on an event cannot say what number the last event should have been. Two READMEs carried the same prediction and are corrected the same way. semantics_version moves 1.1 to 1.2: a new guarantee is a change to the frozen block, so agent-platform's two derived contracts need their derived_from pointers updated. The guarantee names no event type, because the guarantee is ours and the vocabulary is theirs - JOB_SETTLED is recorded outside frozen, and validates today regardless since event_type refs an open set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implementation ของ RFC-0012 ที่ merge ไปใน #28
ทำอะไร
ทุก job ที่เข้า terminal emit
JOB_SETTLEDเป็นใบสุดท้าย พกsettled_as+event_count·COMPLETEDออกด้วยหลังJOB_COMPLETEDเพื่อให้กฎไม่มีข้อยกเว้นreplay ได้ check ปลาย trail 3 ตัว ซึ่งเป็นชนิดที่เมื่อก่อนมีแค่ตัวเดียว:
UnsettledTrailPrematureSettlementevent_countไม่ตรงกับ trailMiscountedTrail🔍 implementation เจอว่า RFC ผิด 2 จุด — แก้ที่ RFC ไม่ใช่หลบ
1.
event_countต้องนับตามjob_idไม่ใช่ตาม subjectGOVERNANCE_DECISIONมีsubject_type: approvalและsubject_idของตัวเอง — การนับตาม subject จะตัด record ที่ count มีไว้เพื่อจับออกไปพอดี · และreplay_tenantจัดกลุ่มตามjob_idอยู่แล้ว นั่นคือ scope ที่ผู้ผลิตกับผู้อ่านนับชุดเดียวกัน2. ตัวอย่างที่ RFC ยกไว้ว่า count จับได้ — มี check เดิมจับอยู่แล้ว
RFC เขียนว่า "a missing
GOVERNANCE_DECISIONpasses today" · ไม่จริง —UnauditedDecisionจับอยู่พอไล่จริงจัง: vocabulary วันนี้ไม่มี record ตัวไหนที่หายแล้วมีแต่ count จับได้เลย ลบใบไหนออกจาก trail จริงก็มี check อื่นยิงก่อนทุกใบ
สิ่งที่ count จับได้จริงวันนี้คือ ทิศ "ใบเกิน" — record ที่ปลอมหรือซ้ำ ซึ่งไม่มีอะไรเรียกหา · และมันจะขยายไปทิศ "ใบหาย" เมื่อ
TASK_ASSIGNED·EXECUTION_STARTED·EXECUTION_FAILEDเริ่มถูก emit เพราะสามตัวนั้นไม่มี structural check ของตัวเองมีเทสต์ที่ assert เรื่องนี้ตรง ๆ (
test_todays_vocabulary_has_nothing_the_count_alone_would_miss) เพื่อให้คำกล่าวอ้างนี้ยังซื่อสัตย์เมื่อ vocabulary โตทั้งสองข้อเขียนเป็น Correction block ใน RFC พร้อมเหตุผล ไม่ได้แก้เงียบ ๆ
เทสต์เดิมที่บันทึกบั๊กนี้ไว้ กลับด้านแล้ว
simulation/testsมีเทสต์ชื่อtest_a_trail_cut_short_at_a_terminal_other_than_completed_still_replays— บันทึกรูนี้ไว้เป็นพฤติกรรมที่คาดหวัง พร้อมทำนายว่าทางแก้คือ per-job sequence number ในevent/v1ตอนนี้เป็น regression test ของทางแก้ และเขียนไว้ว่าคำทำนายนั้นผิด — ADR-0015 พิสูจน์ว่าเลขบน event บอกไม่ได้ว่าใบสุดท้ายควรเป็นเลขอะไร · สิ่งที่ปิดได้จริงไม่ต้องแก้ contract เลยสักตัว
README อีก 2 ไฟล์ (
packages/observability·simulation) เขียนคำทำนายเดิมไว้เหมือนกัน แก้ทั้งคู่พร้อมเหตุผลconformance ตรวจของจริง
check ตัวที่สามตัดใบท้ายของ trail จริงแล้วดูว่าถูกจับ — check ที่พังไม่ได้ก็ไม่ได้ตรวจอะไร
semantics_version1.1 → 1.2guarantee ใหม่คือการเปลี่ยนใน
frozenblock → version ต้องขยับ →approval/v1และevent/v1ของagent-platformต้องอัปเดตderived_from.semantics_versionเป็น"1.2"ระหว่างที่ยังไม่อัปเดต drift check ฝั่งเขาจะแดง เหมือนตอน 1.0 → 1.2 ที่ทำมาแล้ว · ลำดับคือ merge ฝั่งเราก่อน แล้วเปิด PR ฝั่งเขาทันที (ผมทำต่อให้ได้)
guarantee ไม่ระบุชื่อ event type โดยเจตนา — guarantee เป็นของเรา vocabulary เป็นของเขา ·
JOB_SETTLEDบันทึกไว้ นอกfrozenในimplementation_status· payload validate ผ่านอยู่แล้วเพราะevent_typeอ้างEventTypeNameชุดเปิด การเพิ่มเข้า enum เป็น additive ฝั่งเขา ทำเมื่อไหร่ก็ได้ ไม่มีอะไรรอบั๊กที่เกือบหลุด
ตอนแรกผมตั้งชื่อตัวแปรใน
replay_jobว่าsettledซึ่งชนกับตัวแปรเดิมที่มีอยู่แล้ว (หมายถึง approval ที่เพิ่ง settle) ทำให้ 9 เทสต์แดงด้วยPrematureSettlementบน job ที่ยังไม่ terminal · เปลี่ยนเป็นclosingพร้อม comment บอกว่าทำไมImpacted planes
Control — emit เพิ่มหนึ่งใบตอน settle · state/transition/guard ไม่ขยับสักจุด
Observability — event type ใหม่ + check ปลาย trail 3 ตัว
Risk analysis
event_countนับคนละชุดกับผู้อ่าน (high) → นิยาม+ทดสอบว่านับตามjob_id· มีเทสต์ที่ยืนยันว่า trail ครอบมากกว่าหนึ่ง subject จริง เพื่อให้เทสต์นี้ไม่กลายเป็น tautologyevent/v1platform_rulesสั่งให้เก็บ type ที่ไม่รู้จักไว้และข้ามการตีความอยู่แล้วsemantics_versionขยับทำให้ฝั่งเขาแดงชั่วคราว (medium) → รู้ตัว · ลำดับ merge ชัด · เคยทำมาแล้วรอบ 1.0 → 1.1JOB_COMPLETEDไม่ใช่ใบสุดท้ายอีกต่อไป (low) → เทสต์ที่ assert ตำแหน่งถูกแก้ให้ assert ทั้งคู่ตามลำดับ · ความหมายของมันไม่เปลี่ยนRelated
#26 · #28 (RFC) · agent-platform#23 · ADR-0015
🤖 Generated with Claude Code