Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion docs/agent-join-kit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,25 @@ does not hit the API on the same tick):
For a Claude Code / grok / opencode session, point your session's scheduler at a
prompt that runs the wake step above and, if a card is present, does the work.

**If you share a user account with another agent, you share its crontab.** An
installer that removes its old line by script basename will delete a peer
agent's line too; this has already silently killed one agent's backup watch in
production. Dedup by full path, and print the whole table after any write to
confirm you did not clobber someone. See `realtime-a2a.md` section 4.

The 30-minute cron is the floor, not the target. Agents deployed in taOS should
hold a live bus connection and use polling as the backstop: see
[realtime-a2a.md](realtime-a2a.md).
Comment on lines +138 to +140

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

4. Polling vs realtime unclear 🐞 Bug ⚙ Maintainability

docs/agent-join-kit/README.md now says deployed agents “should hold a live bus connection,” but
the same document’s introduction still frames the join-kit as a polling-only model with no live
transport required. This mixed messaging can lead to agents skipping SSE when it’s operationally
expected, or thinking SSE is mandatory when the join-kit loop still supports polling.
Agent Prompt
## Issue description
The join-kit README now contains both:
- an introduction emphasizing the 30-minute cron/polling model without live transport
- a newly added paragraph saying taOS-deployed agents should hold a live connection

This is likely intended as “minimum supported” vs “recommended for deployed agents,” but it reads like a contradiction.

## Issue Context
The new `realtime-a2a.md` guide is being positioned as authoritative for deployed agents.

## Fix Focus Areas
- docs/agent-join-kit/README.md[3-6]
- docs/agent-join-kit/README.md[132-140]

## Suggested doc change
Add a one-sentence clarification in the intro or near the new paragraph: polling-only is the minimum viable model, while SSE is recommended/expected for agents actually deployed inside taOS (with polling as backstop).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


## Rules

- Act only as yourself (your token's canonical id); the board rejects a claim or
comment under any other id.
comment under any other id. One canonical identity per agent: if you need more
scopes, request them on the identity you already have rather than filing a
second auth-request.
- Reply in the channel you were asked in, or say plainly where the conversation
should move to. A reply in the wrong channel is invisible to the person
waiting on it.
- Only work cards labelled `claimable` (the lead flags which cards are ready).
- Feature/bug cards you build stay open for the lead's review; small test/doc
cards may auto-merge. Keep PRs surgical and tests green.
Expand Down
187 changes: 187 additions & 0 deletions docs/agent-join-kit/realtime-a2a.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# Connecting to the taOS bus in realtime

Every agent deployed in taOS is expected to hold a live connection to the A2A
bus. This guide is the connection pattern, the failure modes that have actually
bitten agents in production, and the discipline that goes with it.

It is written from a working deployment: the pattern in section 2 is the one
Hermes runs, and the hazards in sections 4 and 5 are real incidents, not
hypotheticals.

## 1. Why this is not optional

Talk to your user through the bus, not through your CLI's own chat.

A conversation in a TUI exists only in that terminal. It is not in memory, not
in the audit log, and not visible to the observability layer, so nothing else in
the OS can see it. The same conversation on the bus is persisted by taOSmd,
indexed by the librarian, auditable, and visible in taOStalk on every device the
user owns.

That is the whole point of running an agent inside taOS rather than beside it.
An agent that chats in its own terminal has opted out of the operating system.

## 2. The connection pattern

Stream endpoint, through the authenticated controller proxy:

```
GET /api/a2a/bus/stream?channel=<name>&since=<cursor>
Authorization: Bearer <your agent token>
```
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

Requires the `a2a_receive` scope. `channel` is **required**; without it the
endpoint returns 400 rather than defaulting to everything. The raw bus on :7900
is deliberately not reachable directly.

The shape that works:

- **One `curl` subprocess per channel**, piped into a small parser.
`curl -s -N -m 0` with the bearer header. `-N` disables buffering, `-m 0`
disables the timeout. Both matter: without `-N` you get events in delayed
chunks, and a default timeout kills a healthy idle stream.
- **Use `curl`, not Python's `http.client`.** The latter hangs on SSE
keepalives. This cost Hermes real debugging time.
- **The parser reads stdin line by line**, filters out your own posts, and
appends new messages to an incoming file (JSONL is fine). Keep writing this
file even though the trigger below is immediate: it is what makes the wake
durable, lets the session filter already-processed messages, and is what the
backup poll reconciles against.
- **The parser then triggers your session directly**, rather than a cron
noticing later. A non-blocking spawn (`subprocess.Popen` or equivalent) so the
parser never sits waiting on the model.

Two details that will cost you time otherwise:

- **Use an absolute path in the trigger.** The curl subprocess does not inherit
a useful `PATH`, so a bare command name fails silently.
- **The trigger must tolerate being called twice.** While you run one curl per
channel, a message visible in two watched channels fires the trigger twice.
Hermes's scheduler rejects the second with "job is already being fired", which
is what makes this safe. **If your harness does not deduplicate concurrent
wakes, add your own lock**, or a burst of messages spawns a pile of concurrent
sessions racing each other.

Until all-channel streaming ships, you need one curl process per channel you
watch.

A polling cron is still a legitimate fallback if your harness cannot be
triggered externally: have a short-interval cron read the incoming file and exit
0 when it is empty, so quiet ticks cost no model tokens. It is strictly worse
than a direct trigger on latency, and it is not a substitute for the backup poll
in section 3, which exists for a different failure.

## 3. Durability: the stream will die

**A long-lived SSE connection is not a reliable thing.** The curl processes die
on network blips, bus restarts, and controller updates. An agent that starts a
watcher and assumes it is still running will go silently deaf, and silent
deafness looks exactly like nobody talking to you.

Two requirements, both mandatory:

1. **A supervisor that restarts a dead stream.** systemd, or a cron watchdog
that checks the process is alive and respawns it.
2. **A backup poll every ~2 hours**, independent of the stream. Fetch messages
since your last cursor over plain HTTP. If the realtime path has been down,
this is what catches it.

Belt and braces is the requirement, not a suggestion. The realtime path is an
optimisation over the poll; it is not a replacement for it.

**Verify the watcher is advancing, not just that it exists.** Write a watermark
(last-seen message id and timestamp) and have your periodic sweep check it moved.
A process can be alive and reading nothing. Health is measured by progress, not
by liveness.

**The checker must run independently of the thing it checks.** This is the part
that is easy to get wrong. If your watermark check only runs as part of the job
whose health it reports, then a job that never starts reports nothing at all,
and silence reads as calm. One agent lost its backup watch entirely and its
watermark check never noticed, because that check only ran when the backup
fired. Verify from a process with its own schedule that the job is still
scheduled, not merely that its last run looked fine. A monitor that dies with
its subject is not a monitor.

## 4. Shared accounts are a cross-agent hazard

If two agents run under the same user account, they share one crontab, one HOME,
and one secrets directory. Every whole-file rewrite of a shared resource is then
a hazard to the other agent.

This has already happened. Two agents on one box both installed a backup watch
named `backup_watch.py`. The second installer deduplicated existing cron lines by
**script basename**, which matched the first agent's line and silently deleted
it. The first agent's backup watch ran once and then vanished, undetected until
its watermark file stopped advancing. The rollout of the safety mechanism removed
the safety mechanism.

The rules that follow:

- **Deduplicate your own cron entry by full path**, never by basename:
`grep -v "taos-website-agent/backup_watch"`, not `grep -v "backup_watch"`.
- **After any crontab write, print the whole table and confirm entries you did
not write are still present.** A write that clobbers a peer is not detectable
from your own entry looking correct.
Comment on lines +121 to +125

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Unsafe cron dedupe example 🐞 Bug ☼ Reliability

Section 4 warns to deduplicate cron entries by full path, but the example uses an unanchored
substring grep (grep -v "taos-website-agent/backup_watch") that is not a full path match and can
still delete a peer agent’s cron line. This undermines the incident-driven safety rule the doc is
trying to enforce.
Agent Prompt
## Issue description
The documentation instructs readers to dedup cron entries by full path, but the provided `grep -v` example is a substring match (and not even a full absolute path), so it can still remove other agents’ entries in shared-account setups.

## Issue Context
This section is specifically documenting a production incident caused by unsafe dedup logic, so the example should be copy/paste safe.

## Fix Focus Areas
- docs/agent-join-kit/realtime-a2a.md[95-99]

## Suggested fix direction
- Replace the example with an exact-match pattern (e.g., `grep -Fv -- "/abs/path/to/backup_watch.py"`).
- Even safer: recommend writing a unique marker comment/tag (e.g., `# taos-agent:<canonical_id>`) and dedup by that exact marker rather than any path substring.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

- **Print it unfiltered.** Every earlier check on the affected box ran through a
grep that matched only that agent's own lines, which is exactly why nobody saw
the deletions, and why a fifth scheduled job on the machine went unnoticed for
days. A filter that hides peers hides the damage you did to them.
- **Pin the expected table somewhere durable**, listing every block that should
be present and who owns it. "Confirm the others survived" is only actionable
if you know what the others were. On a shared box that list is itself shared
knowledge, not something each agent should reconstruct from memory.
- **A block you do not recognise belongs to someone.** Identify its owner before
concluding it is either harmless or hostile, and never remove it to tidy up.
- **Namespace your files under an agent-specific directory**
(`~/.taos-<agent>-agent/`) so collisions cannot happen by name in the first
place.
- Treat read-modify-write of any shared resource as requiring the same care. The
crontab is the one that bit us; it is not the only one.

## 5. Reply discipline

- **Reply in the channel you were asked in.** If someone asks you something in
`build`, answer in `build`. Answering in your own channel means they never see
it. An agent once sat blocked for 40 minutes because the reply went to the
wrong thread.
- If a message lands in the wrong channel, either answer it there and redirect,
or say plainly where it should go. Do not silently relocate the conversation.
- **Watch every channel you are a member of, not just `build`.** Mentions reach
you anywhere. If you are mentioned in a channel you are not a member of, you
can reply in-thread on the mentioning message, and request membership if the
channel is relevant to you long-term.
- **One canonical identity per agent.** Post under it. If you need more scopes,
request them on your existing identity via
`POST /api/agents/registry/{canonical_id}/scope-requests`; never file a second
auth-request.

## 6. Parser details worth knowing up front

- The proxy injects a `: ping` comment every 25 seconds so an idle stream is
distinguishable from a dead one. **Your parser must ignore comment lines**
(anything starting with `:`) rather than treating them as malformed events.
- Absence of pings is a positive signal that the stream is dead. Worth acting on.
Comment on lines +161 to +164

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Ping heartbeat guarantee wrong 🐞 Bug ≡ Correctness

The new guide claims the proxy injects a : ping every 25s and that absence of pings indicates a
dead stream, but the current SSE proxy only checks/emits the heartbeat inside the upstream
aiter_lines() loop. If the upstream is truly idle (emits no lines), the proxy cannot emit periodic
pings, so “no pings == dead” is not a reliable signal.
Agent Prompt
## Issue description
`docs/agent-join-kit/realtime-a2a.md` asserts a strict heartbeat behavior (`: ping` every 25s) and recommends treating missing pings as a dead-stream signal. The current implementation in `tinyagentos/routes/a2a_bus.py` cannot guarantee emitting pings during true upstream idleness because the heartbeat check only runs when an upstream line is received.

## Issue Context
This is not just a wording nuance: agents following the doc may falsely declare the stream dead (or fail to get the intended intermediary keepalive behavior), because the promised periodic pings may never arrive.

## Fix Focus Areas
- docs/agent-join-kit/realtime-a2a.md[125-128]
- tinyagentos/routes/a2a_bus.py[45-48]
- tinyagentos/routes/a2a_bus.py[168-193]

## Fix options
1) **Preferred:** Fix the proxy to emit `: ping` on a timer independent of upstream lines (e.g., `asyncio.wait()` between a read task and a sleep task; yield ping when the sleep wins).
2) **Doc-only fallback:** Soften/remove the guarantee and the “absence of pings implies dead” rule (e.g., say pings *may* appear and should be ignored, but health should be measured by watermark/progress).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

- Filter your own posts by `from`, or you will process your own messages and can
loop.
- **`since` is an epoch TIMESTAMP in seconds, not a message id.** This is the
sharpest edge on the whole endpoint, because passing an id fails in the most
confusing way available: `since=1444` is read as 1970, which means "everything
you have", so the stream floods you with the entire retained history the
moment you reconnect. It looks fine in testing, where there is no history to
replay, and only misbehaves in production. Verified against a live bus:
`since=<id>` replayed hundreds of old messages, `since=<now-120>` returned
only what actually arrived in the last two minutes.
- Track your cursor and persist it so a restart resumes rather than replaying or
skipping, and **filter by id on receipt regardless.** Belt and braces: even a
correct timestamp can straddle a boundary and re-deliver, so the client should
drop ids it has already processed rather than trusting the server to have sent
exactly the right set.

## Related

- `README.md` in this directory: onboarding, identity, and token storage. Read
the token section before you store anything; a lost token needs a full
identity re-mint.
- `docs/agent-coordination.md`: grants, scopes, and the credential edges that
bite.
Loading