Skip to content
C.W.K.
Stream
Lesson 04 of 05 · published

Every Reply Answered

~16 min · acknowledgement, contracts, validation, reporting

Level 0Loose Note
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

The Reply Nobody Re-read

Picture the ordinary way this goes wrong. Dad assigns a todo, and an hour later he thinks of something: use the other API, skip the iPad for now, the name should be different. He adds a reply. A worker deep in its task has no reason to open the todo again, so it closes the item done against the body it read at the start, and the correction surfaces only when someone reads the result. Coop's own doctrine has a name for that kind of reply: one nobody re-reads.

Commit 4e78f1e (2026-10-05) closed that gap with three rules that work together. A reply on active work is delivered to the exact worker. The worker must acknowledge exactly what it was shown. And a done must answer every reply, one by one. Reading is not acknowledging, and acknowledging is not answering.

Delivered: the reply review

When a reply or a body edit lands on a todo that has an active binding, the engine enqueues a reply_review envelope for that binding's session. It carries the current todo, its body and every reply, newest first with the newest marked. If an earlier review is still unsent, it is cancelled and replaced, so the worker gets one fresh picture instead of a stack of stale ones.

The prompt is direct: read the updated body and every reply before continuing; the body is the current task contract, and name and scope changes belong in the body; acknowledge with COOP_READ and this envelope's id in your next final; this does not complete the task; before done, answer every reply in evidence.reply_outcomes. It also carries the same ending rule as every other envelope, so the token goes on its own hidden line: [coop-read]: <COOP_READ id>.

Acknowledged: exactly what was displayed

Engine.acknowledge_replies is strict in a specific way. The envelope must be a reply review for this session at this generation. The native receipt must be a completed final with a native message id. The text must contain COOP_READ followed by this envelope's id, as a whole token. The binding must still be active. Then it acknowledges the message ids listed in that envelope, and nothing else.

That last part is the point. A reply Dad adds one minute after the review went out is not covered by acknowledging the review. It produces its own review, and the worker has to read that one too. And when one final carries both a COOP_READ and a report, the adapter handles the review envelope first, so the acknowledgement is on record before the completion it gates.

Receipts Belong to a Session, Not a Name

Acknowledgements are stored in todo_reply_reviews, keyed by a reader. When the call carries a session, the reader is session: plus that session's id. Only an unbound caller falls back to actor: plus its identity. Two Claude Code sessions both writing as the same actor surface are different readers, so one cannot borrow the other's read. The receipt says that this particular conversation saw these particular messages, which is the only thing worth knowing.

Unread replies then block two transitions. A take, including a batch scope take, refuses with a message that names the unread ids. A done refuses the same way. Even a forced done does not get past it.

Answered: one line per reply

A done on a todo must include reply_outcomes, and the validator in the domain is unforgiving:

  • each entry has exactly three keys: message_id, disposition and reason;
  • disposition is applied or not_applied;
  • reason is non-empty and a single line;
  • no message id appears twice, and the set of ids must equal all of the todo's messages.

Miss one and the done is refused, naming every message id it expected. Pass, and the answers are stored against your receipts and appended to the visible result summary under Reply outcomes:, one line each. Dad does not have to wonder whether his correction was seen. The record says applied or not applied, and why.

not_applied is a real answer, not a failure. A reply might be superseded by a later one, or Dad might have asked a question rather than requested a change. What the rule forbids is silence.

The Body Is the Contract

One more rule keeps the thread from becoming the spec. When a reply changes the name or the scope of the work, whoever writes it also amends the todo body. Replies are discussion; the body is the source of truth. A worker that picks up the todo next week should not need to replay a conversation to learn what it is supposed to build. Dad also ruled out a retroactive pass: the gate applies to active work, and completed todos from before the change are never reopened or audited.

Delivered, acknowledged and answered are three different receipts, and only the last one closes the loop. A system that stops at delivered tells you the message arrived. One that stops at acknowledged tells you it was seen. Only an answer per message tells the person who wrote it what happened to what they said.

Code

From the domain, trimmed: who a reader is, and what a complete set of answers looks like·python
def _reply_reader(actor=None):
    session = _context.metadata('session')
    return 'session:' + session if session else 'actor:' + get_identity(actor)

def _reply_outcomes(conn, todo_id, actor, outcomes):
    _require_reply_read(conn, todo_id, actor)
    messages = _todo_messages(conn, todo_id, actor)
    outcomes = [] if outcomes is None else outcomes
    validated = {}
    for entry in outcomes:
        if (not isinstance(entry, dict) or set(entry) != {'message_id', 'disposition', 'reason'}
            or type(entry['message_id']) is not int or entry['message_id'] in validated
            or entry['disposition'] not in ('applied', 'not_applied')
            or not isinstance(entry['reason'], str) or not entry['reason'].strip()
            or '\n' in entry['reason'] or '\r' in entry['reason']):
            raise LifecycleError('Each reply needs one applied/not_applied outcome and a nonempty one-line reason')
        validated[entry['message_id']] = entry
    if set(validated) != {m['id'] for m in messages}:
        raise LifecycleError(f'todo #{todo_id}: answer every reply in reply_outcomes before completing; message IDs: '
                             + ', '.join(str(m['id']) for m in messages))
    ...
Two finals from one worker, made-up ids: acknowledge now, answer at done·text
--- a final after a reply review arrives (the work continues) ---
아빠, 답글 봤어. iPad는 빼고 iPhone만 먼저 할게.

[coop-read]: <COOP_READ r-7f3a>

--- the done, turns later ---
아빠, iPhone 설정 화면 끝났어. 커밋 푸시했고 TestFlight 빌드 올렸어.
아빠가 할 일은 빌드 확인뿐이야.

[coop-report]: <{"coop_reports":[{"binding_id":"b-1","outcome":"done","body":"iPhone settings screen shipped; iPad deferred per reply.","for_dad":"아빠, iPhone 설정 화면 끝났어. 커밋 푸시했고 TestFlight 빌드 올렸어. 아빠가 할 일은 빌드 확인뿐이야.","evidence":{"commits":["abc1234"],"reply_outcomes":[{"message_id":41,"disposition":"applied","reason":"Dropped iPad from this pass as asked"},{"message_id":40,"disposition":"not_applied","reason":"Superseded by reply 41"}]}}]}>

External links

Exercise

Pick a ticket in your own tracker that collected comments after it was assigned. Write the reply outcomes its closer should have produced: one line per comment, applied or not applied, with a reason. Then check whether your tracker could have refused the close until those lines existed, and whether its read state is per person or per session.
Hint
Most trackers record that a comment was posted and maybe that it was viewed. Few record what the assignee did about it. If you can only write the outcomes by digging through commits, that digging is the work a per-reply answer saves the next reader.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.