Skip to content

Implement RFC-0012: JOB_SETTLED closes every terminal - #29

Merged
monthop-gmail merged 1 commit into
mainfrom
feat/job-settled
Aug 21, 2026
Merged

Implement RFC-0012: JOB_SETTLED closes every terminal#29
monthop-gmail merged 1 commit into
mainfrom
feat/job-settled

Conversation

@monthop-gmail

Copy link
Copy Markdown
Owner

Implementation ของ RFC-0012 ที่ merge ไปใน #28

core          349 passed · 100%
observability 126 passed ·  99%
simulation     81 passed · e2e_flow exit=0
conformance    22/22 · FAIL=0

ทำอะไร

ทุก job ที่เข้า terminal emit JOB_SETTLED เป็นใบสุดท้าย พก settled_as + event_count · COMPLETED ออกด้วยหลัง JOB_COMPLETED เพื่อให้กฎไม่มีข้อยกเว้น

replay ได้ check ปลาย trail 3 ตัว ซึ่งเป็นชนิดที่เมื่อก่อนมีแค่ตัวเดียว:

อาการ error
terminal แต่ไม่มีใบปิด UnsettledTrail
มีใบปิดทั้งที่ job ยังเดินอยู่ PrematureSettlement
event_count ไม่ตรงกับ trail MiscountedTrail

🔍 implementation เจอว่า RFC ผิด 2 จุด — แก้ที่ RFC ไม่ใช่หลบ

1. event_count ต้องนับตาม job_id ไม่ใช่ตาม subject

GOVERNANCE_DECISION มี subject_type: approval และ subject_id ของตัวเอง — การนับตาม subject จะตัด record ที่ count มีไว้เพื่อจับออกไปพอดี · และ replay_tenant จัดกลุ่มตาม job_id อยู่แล้ว นั่นคือ scope ที่ผู้ผลิตกับผู้อ่านนับชุดเดียวกัน

2. ตัวอย่างที่ RFC ยกไว้ว่า count จับได้ — มี check เดิมจับอยู่แล้ว

RFC เขียนว่า "a missing GOVERNANCE_DECISION passes 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 ตรวจของจริง

[3] ใบปิดท้ายของทุก terminal — RFC-0012
  ok  closure: 5 job ที่จบแล้วมีใบปิดท้ายครบ และ event_count ตรงกับ trail
  ok  closure: ครอบครบทั้ง 4 terminal — ไม่ใช่แค่ทางที่จบสำเร็จ
  ok  closure: trail ที่ถูกตัดท้ายถูกจับได้ (COMPLETED)

check ตัวที่สามตัดใบท้ายของ trail จริงแล้วดูว่าถูกจับ — check ที่พังไม่ได้ก็ไม่ได้ตรวจอะไร

⚠️ ต้องประสาน — semantics_version 1.1 → 1.2

guarantee ใหม่คือการเปลี่ยนใน frozen block → 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 จริง เพื่อให้เทสต์นี้ไม่กลายเป็น tautology
  • ทุก consumer ที่อ่าน trail เจอ type ใหม่ (medium) → event/v1 platform_rules สั่งให้เก็บ type ที่ไม่รู้จักไว้และข้ามการตีความอยู่แล้ว
  • semantics_version ขยับทำให้ฝั่งเขาแดงชั่วคราว (medium) → รู้ตัว · ลำดับ merge ชัด · เคยทำมาแล้วรอบ 1.0 → 1.1
  • JOB_COMPLETED ไม่ใช่ใบสุดท้ายอีกต่อไป (low) → เทสต์ที่ assert ตำแหน่งถูกแก้ให้ assert ทั้งคู่ตามลำดับ · ความหมายของมันไม่เปลี่ยน

Related

#26 · #28 (RFC) · agent-platform#23 · ADR-0015

🤖 Generated with Claude Code

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant