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¶
| 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
-sXsuffix (shared main checkout) → warn, pass. [sX]prefix matches the-sXworktree 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:
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.