Skip to content

Inter-session comms — reference

Dry, lookup-oriented detail. For why any of this is shaped the way it is, read explanation.md. The normative source is ADR-010; where this page and the ADR disagree, the ADR wins.


File & script inventory

Path Role Portable as-is?
docs/ai/sessions/active.md Claim scoreboard + presence (Tier 1) Template — keep structure, clear rows
docs/ai/sessions/queue/to-<sX>.md Addressed message queue (Tier 1) Created on demand, one per recipient
docs/ai/sessions/queue/to-all.md Broadcast queue Optional
docs/ai/parallel-sessions.md Operational doctrine (Layers 0–11) Adapt project-specific bits
docs/developers/adr/ADR-010-inter-session-comms.md Normative contract Adapt project names / pool
infrastructure/scripts/start-session.sh Worktree + claim (Layer 0) Edit hardcoded project name + name pool
infrastructure/scripts/check-worktree-prefix.sh commit-msg backstop Portable verbatim (no project name)
infrastructure/scripts/session-heartbeat.sh post-commit presence beacon Edit hardcoded project name
lefthook.yml Hook registration (commit-msg, post-commit) Add the two hook entries

Session identity

Identity Form Lifetime Used for
Technical id sA, sB, sC, sD, sE Per-process (ephemeral) Worktree suffix, commit prefix, hook check, branch, scoreboard column
Display name Project-themed ASCII string Durable (re-adopted on re-claim) Human coordination, tracker assignee, friendly addressing

Display-name rules (firm): plain ASCII letters [A-Za-z] only · no accents, diacritics, or special chars · ≤ 12 characters · no spaces · unique within the project's pool. Validated by start-session.sh's validate_name against ^[A-Za-z]{1,12}$.

po-platform name pool (the worked example): places — Sintra, Douro, Algarve, Tejo, Madeira, Porto, Lisboa, Coimbra, Cascais, Faro; seafaring — Caravela, Bussola, Sextante, Quadrante, Cabo.

sD/sE are reserved for the rare 4+-session case.


Address resolution

A message's to: field accepts three forms; all resolve against active.md:

Form Example Resolves via
Technical id to:sA active.md row whose Session cell matches sX
Display name to:Sintra active.md row whose Session cell matches the name
Lane alias to:web-presence-owner active.md row whose capabilities claim that lane
Broadcast to:all every active row (file: queue/to-all.md)

Tier 1 — message header grammar (queue/to-<sX>.md)

Each message is a markdown block:

## <ISO-8601-ts> · from:<sX> · to:<sY|all> · type:<type> · ref:<ref> · status:sent
<body — markdown>
→ status:seen <ts> · acked <ts> · resolved <ts>
Field Values / meaning
timestamp ISO-8601 UTC, e.g. 2026-05-31T11:40Z
from sender session id
to recipient id, display name, lane alias, or all
type one of msg · task · handoff · ack · question · answer · relay
ref a Riff #, commit SHA, file path, or
status starts at sent; the recipient/owner advances it in place

type:relay specifically tags a human→agent message that was injected via Telegram and relayed into the queue — it keeps the human bridge auditable.


Tier 1 — the ack state machine

sent ──► seen ──► acked ──► resolved
                   └──► superseded
Status Set by Meaning
sent sender message written & committed
seen recipient message was read (proves it was caught)
acked recipient recipient will act on it
resolved recipient done
superseded recipient obsoleted by a later message

The recipient/owner advances the → status: line in their own commit. The sender later reads the advanced status on git pull — so a relay is lossless: the sender confirms the ball was caught rather than assuming delivery.


Tier 1 — active.md row schema (v4 / Layer 6, 6 columns)

| Session · Name | Riff / Task | Branch / Commit prefix | Claimed (UTC) | Presence (last_seen · caps) | Scope |
Cell Content Example
Session · Name sX · DisplayName sA · Sintra
Riff / Task what's being worked Riff #221 active-channel
Branch / Commit prefix worktree branch + prefix worktree session-sA → direct-to-main, [sA]
Claimed (UTC) ISO-8601 claim time 2026-05-31T11:40Z
Presence <last_seen-ts> · <caps> 2026-05-31T12:05Z · has-telegram, comms-lane
Scope files / area touched docs/developers/inter-session-comms/

The session-heartbeat.sh post-commit hook rewrites only the leading timestamp of the Presence cell of the worktree's own live row. Rows below the first <!-- sX self-closed ... --> marker are treated as history and never bumped.


Ownership verbs (single-owner invariant)

A unit of work (a Riff, or a lane) has at most one owner at a time, recorded in both the active.md row and the Riff assignee.

Verb Action
claim write the row / set the assignee
release clear the row / clear the assignee
handoff(to:sX) send a type:handoff message and reassign

There is no distributed lock — the protocol is cooperative, and the visible claim is the lock.


The three mechanical scripts

start-session.sh

bash infrastructure/scripts/start-session.sh                          # auto-pick lowest unused sX + first unused name
bash infrastructure/scripts/start-session.sh -s sC                    # claim a specific sX, auto-pick name
bash infrastructure/scripts/start-session.sh -s sC -n Bussola         # claim sX + specific display name
bash infrastructure/scripts/start-session.sh -s sC -r "Riff #181" -c "has-telegram,backend-lane"
Flag Meaning
-s sX claim a specific session letter (else lowest unused)
-n <name> specific display name (else first unused from pool)
-r '<scope>' initial scope text for the claim row
-c '<caps>' capabilities (comma-separated)
-h help
Exit code Meaning
0 success — cd path printed
1 argument / repo-state error
2 already inside a worktree (use that terminal)

Behaviour: detects the unused sX from active.md; creates the worktree at ../<project>-sX from origin/main on branch session-sX (or re-enters an existing one); resolves & validates a display name; appends the v4 claim row and atomic-commits it as [sX] docs(ai-sessions): claim sX at <UTC>; symlinks the main checkout's lefthook into the worktree's (gitignored) node_modules so the commit-msg/post-commit hooks actually fire there (Riff #228).

check-worktree-prefix.sh (commit-msg hook)

Receives the commit-message file path as $1. Logic:

  • No [sX] prefix in the subject → pass (merge/revert/system commits).
  • Worktree has no -sX suffix (shared main checkout) → warn, pass.
  • [sX] prefix matches the -sX worktree suffix → pass.
  • Mismatch → abort (exit 1). Bypass with git commit --no-verify.

session-heartbeat.sh (post-commit hook)

No-ops (exit 0) when: not inside a <project>-sX worktree · active.md missing · no live row matches the prefix · python3 unavailable. Otherwise bumps the Presence-cell timestamp. Never stages or commits the change itself.


Lefthook registration

commit-msg:
  commands:
    worktree-prefix-match:
      run: bash infrastructure/scripts/check-worktree-prefix.sh "{1}"

post-commit:
  commands:
    session-heartbeat:
      run: bash infrastructure/scripts/session-heartbeat.sh

Install once per clone: npm install && npx lefthook install. Global bypass: LEFTHOOK=0 git commit ... or git commit --no-verify.


Push model

Commits in a session worktree are local. Publish with:

git push origin HEAD:main

This fast-forwards origin/main from the session branch. If main moved: git fetch && git rebase origin/main, then retry.


Reap rule (Layer 3)

Any active.md row older than 6 hours with no commits on its named branch (or no [sX] commits on main in that window) may be removed by another session. The reaper logs the reap (timestamp + reason) but does not inherit the work — it just frees the claim so the Riff can be re-picked.


Deferred / rejected (ADR-010 "Upgrade triggers")

Option Status Trigger to revisit
File-watcher push (git log origin/main..main -- queue/to-<me>.md) Deferred — recommended first upgrade poll-on-pull latency hurts
Postgres session_bus + LISTEN/NOTIFY Deferred file-watcher insufficient; real-time push needed
A2A (Agent Cards + Tasks) Deferred cross-machine / many-agent / untrusted peers
RabbitMQ as the bus Rejected (never — liveness bus, wrong fit for idle/async sessions)
Riff active-channel upgrade — P1 unread inbox · P2 session-addressing · P3 handoff/ack · P4 @mentions · P5 presence Implemented (draft PRs, cc-platform #233–#236); pending merge + deploy Riff #221; approach B (no to_session column), explicit beacon. See ADR-010 implementation note.

Cross-project comms (Layer 7)

Layers 0–6 are intra-project (sA/sB/sC in one repo). For sessions in different projects to coordinate, only the shared tasks-prod instance is reachable (the git blackboard is per-repo; Telegram is single-holder). Channel:

Element Value
Bus a dedicated tasks-prod project, "Agent Comms (cross-project)"
Identity project-qualified handle: <tag>:<sX> / <tag>:<DisplayName> (po:sA, cc:Bicho); <tag>:* broadcasts
Message one task per thread; title <from> → <to>: <subject>; labels from:<tag>/to:<tag>; body = ADR §1 header; comments = replies + ack
Inbox tasks on that project labelled to:<your-tag> not done
Status convention-only now (zero new code); mechanical version (project-qualified session_presence + cross-project resolve_session) filed as RIFF-004

Full normative spec: ADR-010 §"Cross-project extension (Layer 7)" + the board's pinned CONVENTION task.