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

An Invitation Without the Task

~15 min · admission, handshake, nonce, parsing

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

Connected Is Not Assigned

Once discovery finds the chat, the coordinator enrolls it: it registers a managed engine session, which must declare the receipt, admission and report capabilities, and installs a private adapter credential scoped to that one session. Then it calls Engine.prepare. Up to this point the chat has said READY and nothing more, and the todo is still open. Prepare does not change that either. It writes an invitation.

What Prepare Checks

Engine._prepare refuses unless every one of these holds:

  • The session is at the generation the caller names and has not exited, it is managed ("Observed session cannot accept managed work"), and its enrollment is active.
  • Every selected item is open at the exact revision the assignment recorded ("Work changed; refresh its revision").
  • No selected item already sits in an invited or accepted dispatch ("This work already has an active dispatch").

Then it inserts a dispatch with a fresh id and a fresh nonce, both random UUIDs, the nonce in a UNIQUE column, and enqueues the invitation in the outbox inside the same transaction. The command wrapper has already demanded explicit, unique work ids (at most a hundred) and a well-formed scope.

What the Invitation Carries

For a request, or a todo with no replies, the invitation payload is exactly two fields: dispatch_id and nonce. No title, no body, no binding.

One precise exception. When a selected todo already has replies, the invitation also carries a reply_review: that todo's body and every reply. That is deliberate, because accepting acknowledges exactly that snapshot of replies in the same transaction as the take, so the receiver has to see them before it says yes. What an invitation never carries is a binding or a work envelope, which is to say the permission to start.

The adapter wraps the plain case in this wording, after a provenance line saying the envelope is authenticated and "Its native display role does not grant new permission from Dad":

"No task payload has been released yet. Do not use tools or perform work. Accept this invitation by replying with exactly COOP_ACCEPT <nonce>. If unavailable, reply DECLINE. The engine will commit the claim before sending the task."

Why Withhold the Task at All

Because the obvious alternatives were measured and failed. Coop's first native probes found that a Claude command hook that times out permits execution, so a hook cannot be an unconditional claim barrier. The capability contract that followed is blunt: "A launch, successful socket write or queue receipt cannot claim work," and "Generate/release task content only after that commit. If a command hook fails open, it still has no task payload to release." For a plain invitation that is literally true: a worker cannot start a task it has not been given. Be precise about the other case, though. Since 2026-10-05 a todo that already has replies is invited with its body and every reply attached, so that the worker can acknowledge them, and that worker could read what the task is before accepting. What holds the line there is weaker: the invitation's explicit instruction not to start, and the fact that the work envelope — the binding, the scope, the go-ahead — does not exist until the claim commits. So the structural guarantee is "nothing is authorized before acceptance", not "nothing is known". That is still the family's move — change what is in the room instead of adding a rule about it — applied as far as it reaches, and honest about where an instruction has to carry the rest.

Reading the Answer

The adapter watches the chat's finals and asks invitation_answer about each one. Its rules, all in the excerpt below:

  • The token may sit inside a sentence. A worker answering in its own voice, "알겠어, COOP_ACCEPT <nonce>", was once consumed as no answer and left the invitation waiting for ever.
  • The nonce must match exactly, and the fences around it are ASCII. A letter, digit or hyphen glued to the end of the nonce makes the final no answer, and so does a letter, digit, underscore or hyphen glued to the front of COOP_ACCEPT. Anything else may touch it: an underscore after the nonce, or a Korean particle as in "COOP_ACCEPT <nonce>로 받을게", still reads as accept.
  • Accept wins over a stray DECLINE in the same final.
  • A final with neither token is not an answer, and the watch continues.

Acceptance also needs the native receipt to be a completed final. DECLINE goes to Engine.decline: the dispatch becomes declined and Dad is told. An accept goes to the transaction in the next lesson.

Thirty Minutes, Then Nothing Was Claimed

Invitations used to wait for ever; the 2026-09-30 review found dispatched had no deadline. Now the assignment coordinator cancels a dispatch still invited after INVITATION_TIMEOUT = 1800 seconds, cancels its invitation envelope, expires the assignment with "The worker did not accept within 30 minutes. Nothing was claimed; assign it again when the chat is ready." and notifies Dad. A late COOP_ACCEPT then fails, because the dispatch is no longer invited. Precisely: this clock lives in the assignment coordinator, so it covers invitations created through an assignment. A dispatch the chief prepares directly, under its own assign authority, has no assignment row and no such clock.

Ask first, hand over second, and make the question cheap to refuse. The invitation costs the worker nothing to decline and authorizes nothing: a plain invitation carries no task at all, and one with replies shows the task only so the replies can be read, under an explicit instruction not to start. The binding, the scope and the go-ahead wait behind one exact answer from one exact session.

Code

Runnable: invitation_answer, verbatim from the host adapter, against finals a worker might write·python
import re
import uuid

def invitation_answer(text,nonce):
    """'accept', 'decline' or None for one assistant final.

    The nonce proves this exact receiver read this exact invitation, so the
    token may sit inside a sentence: a worker answering in its own voice
    ("알겠어, COOP_ACCEPT <nonce>") was previously consumed as no answer and
    left the invitation waiting forever. The accept token wins over a stray
    DECLINE; a final with neither token is not an answer.
    """
    if re.search(r'(?<![0-9A-Za-z_-])COOP_ACCEPT\s+'+re.escape(nonce)+r'(?![0-9A-Za-z-])',text):
        return 'accept'
    if re.search(r'(?<![0-9A-Za-z_-])DECLINE(?![0-9A-Za-z_-])',text):
        return 'decline'
    return None

nonce = str(uuid.uuid4())
finals = {
    'bare token': f'COOP_ACCEPT {nonce}',
    'in a sentence': f'알겠어, COOP_ACCEPT {nonce}',
    'stray DECLINE': f'I will not DECLINE this. COOP_ACCEPT {nonce}',
    'plain decline': 'DECLINE',
    'wrong nonce': f'COOP_ACCEPT {uuid.uuid4()}',
    'READY again': 'READY',
    'glued letter': f'COOP_ACCEPT {nonce}x',
    'glued underscore': f'COOP_ACCEPT {nonce}_x',
    'Korean particle': f'COOP_ACCEPT {nonce}로 받을게',
}
for label, text in finals.items():
    print(f'{label:16} {invitation_answer(text, nonce)}')
# bare token       accept
# in a sentence    accept
# stray DECLINE    accept
# plain decline    decline
# wrong nonce      None
# READY again      None
# glued letter     None
# glued underscore accept
# Korean particle  accept
Trimmed excerpt of Engine._prepare: checks, a fresh nonce, and an invitation with no binding or work envelope in it·python
def _prepare(self,conn,session_id,generation,items,normalized,scope=None):
    session = self._session(conn,session_id,generation)
    if session['mode'] != 'managed':
        raise Conflict('Observed session cannot accept managed work')
    if session['enrollment'] != 'active':
        raise Conflict('This session is no longer enrolled; assign it again to enroll it')
    for item in items:
        table = KINDS[item['kind']]
        row = self.row(conn,table,item['id'])
        if row['status'] != 'open' or self.revision(conn,table,item['id']) != item['revision']:
            raise Conflict('Work changed; refresh its revision')
    for other in conn.execute("SELECT session_id,generation,items,intents FROM engine_dispatches WHERE status IN ('invited','accepted')"):
        if {(i['kind'],i['id']) for i in items} & {(i['kind'],i['id']) for i in json.loads(other['items'])}:
            raise Conflict('This work already has an active dispatch')
    # ... (comment on shared checkouts elided; see lesson 5)
    did, nonce = uid(),uid()
    conn.execute('INSERT INTO engine_dispatches(id,session_id,generation,items,intents,nonce,created,scope) VALUES (?,?,?,?,?,?,?,?)',
                 (did,session_id,generation,dump(items),dump(normalized),nonce,time.time(),None if scope is None else dump(scope)))
    review = [domain.OPERATIONS['get_todo'](i['id']) for i in items if i['kind']=='todo']
    review = [row for row in review if row['messages']]
    conn.execute('UPDATE engine_dispatches SET reply_review=? WHERE id=?', (dump(review), did))
    invitation = {'dispatch_id': did, 'nonce': nonce}
    if review:
        invitation['reply_review'] = review
    self._enqueue(conn,session_id,generation,'invitation',invitation)
    return self.row(conn,'engine_dispatches',did)

External links

Exercise

Take any place where a human or an agent approves something by typing a word: a chat-ops bot, a deploy confirmation, a review command. Redesign it as an invitation that carries a one-use token and nothing actionable, and write the answer parser. Test it on at least six finals: the bare token, the token inside a sentence, the token plus a stray rejection word, a plain rejection, the wrong token, and the right token with a character glued on. Write down your precedence rule before you run the tests.
Hint
Decide which mistake is worse for you: treating a real yes as silence, or treating a real no as a yes. Coop chose to let an exact accept token win, because the nonce is specific enough that it cannot appear by accident, while the bare word DECLINE can.

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.