Skip to content

chore: drop generated artifacts not tracked on master - #233

Open
jaylfc wants to merge 2 commits into
masterfrom
exec/tsk-lmlx2v
Open

chore: drop generated artifacts not tracked on master#233
jaylfc wants to merge 2 commits into
masterfrom
exec/tsk-lmlx2v

Conversation

@jaylfc

@jaylfc jaylfc commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Autonomous build of board card tsk-lmlx2v.

Files:
taosmd/api.py | 4 +
taosmd/http_server.py | 50 ++++-
taosmd/mentions.py | 85 ++++++++
taosmd/remote.py | 20 ++
taosmd/service.py | 197 ++++++++++++++++-
tests/test_a2a_mentions.py | 512 +++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 866 insertions(+), 2 deletions(-)

Summary by CodeRabbit

  • New Features
    • Added mention-aware messaging with support for explicit recipients.
    • Added a mentions feed with pagination, timestamp filtering, and thread context.
    • Added access controls so readers only see messages they’re permitted to view.
    • Added HTTP and client support for retrieving mention feeds.
    • Included mention visibility across reply chains while excluding unrelated messages.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds recipient-aware A2A sending, SQLite-backed mention indexing, filtered mention feeds with thread visibility checks, and authenticated HTTP and remote client access.

Changes

A2A mention feeds

Layer / File(s) Summary
Mention indexing and store lifecycle
taosmd/mentions.py, taosmd/api.py, tests/test_a2a_mentions.py
Adds MentionStore, extracts body mentions and recipients, records message metadata, and initializes the store.
Recipient-aware A2A sending
taosmd/service.py, tests/test_a2a_mentions.py
Adds optional recipients to a2a_send, persists and returns them, and indexes mentions after sending.
Mention feed and thread visibility
taosmd/service.py, tests/test_a2a_mentions.py
Adds mention-feed retrieval, reply-chain and thread-root handling, pagination, and can_read checks.
HTTP and remote feed APIs
taosmd/http_server.py, taosmd/remote.py, tests/test_a2a_mentions.py
Adds GET /a2a/mentions, token-based reader selection, standalone reader selection, validation, and the remote client wrapper.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RemoteClient
  participant HTTPServer
  participant Service
  participant MentionStore
  participant Archive
  RemoteClient->>HTTPServer: GET /a2a/mentions
  HTTPServer->>HTTPServer: Validate token and pagination
  HTTPServer->>Service: Call a2a_mentions_feed with reader
  Service->>MentionStore: Query mention records
  Service->>Archive: Resolve reply chains and thread roots
  Archive-->>Service: Return message records
  Service-->>HTTPServer: Return filtered messages
  HTTPServer-->>RemoteClient: Return messages list
Loading

Possibly related PRs

  • jaylfc/taosmd#225: Introduces recipient handling that this change extends with mention indexing and feeds.
  • jaylfc/taosmd#228: Modifies related A2A recipient handling across service, HTTP, and remote paths.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title describes removing generated artifacts, but the changes primarily add A2A mention indexing, feeds, APIs, and tests. Rename the pull request to describe the primary A2A mention indexing and feed functionality.
Docstring Coverage ⚠️ Warning Docstring coverage is 6.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch exec/tsk-lmlx2v

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gitar-bot

gitar-bot Bot commented Aug 2, 2026

Copy link
Copy Markdown

Important

You are using the Gitar free plan. Upgrade to unlock code review, CI analysis, auto-apply, custom automations, and more.

Gitar

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add A2A mention indexing and authenticated /a2a/mentions feed

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Index @handle mentions (and optional explicit recipient) on A2A message send.
• Add authenticated GET /a2a/mentions feed that returns mention threads (root + reply chain).
• Add service, remote client, and end-to-end HTTP tests for mention behavior and auth.
Diagram

graph TD
  C[Client] --> H["HTTP server"] --> S["Service layer"]
  H --> A{{"Registry auth"}}
  S --> M[("Mentions DB")]
  S --> R[("Archive DB")]
  subgraph Legend
    direction LR
    _c[Client] ~~~ _svc["Service/Handler"] ~~~ _ext{{External/Auth}} ~~~ _db[(Database)]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Derive mentions on demand from archive
  • ➕ No additional SQLite DB/file to manage
  • ➕ Always consistent with archive; no indexing lag/backfill concerns
  • ➖ Requires scanning/parsing many events per request (likely slower at scale)
  • ➖ Harder to support efficient since/limit cursors without an index
2. Store mentions in the archive DB (same SQLite)
  • ➕ Single persistence surface (backup/restore/transactions)
  • ➕ Enables SQL joins/queries without loading all events into Python
  • ➖ Requires archive schema change/migration and tighter coupling
  • ➖ May be harder to keep archive append-only semantics
3. Maintain reply-chain closure (precomputed ancestry)
  • ➕ Makes mention-thread expansion fast and avoids O(N) archive scans
  • ➕ Simplifies thread_root computation at read time
  • ➖ More write-time complexity and additional index/storage
  • ➖ Needs careful handling of edits/corruption/cycles

Recommendation: The PR’s approach (append-only mention index + service-side reply-chain expansion) is a pragmatic first step and is well-covered by tests, especially around auth and sibling exclusion. The main follow-up to consider is performance: a2a_mentions_feed currently scans all A2A events (limit=100k) to build reply chains and thread roots; if this endpoint becomes hot, moving reply-chain derivation into a DB query or maintaining lightweight ancestry metadata would significantly reduce read amplification.

Files changed (6) +866 / -2

Enhancement (5) +354 / -2
api.pyInitialize MentionStore and add it to the shared stores cache +4/-0

Initialize MentionStore and add it to the shared stores cache

• Extends _ensure_stores() to create and initialize a new MentionStore backed by a2a-mentions.db. Exposes it via the stores dict under the key "mentions" for service/http usage.

taosmd/api.py

http_server.pyAdd GET /a2a/mentions endpoint with registry-auth reader derivation +49/-1

Add GET /a2a/mentions endpoint with registry-auth reader derivation

• Documents and routes a new /a2a/mentions GET endpoint. Implements a handler that parses since/limit, enforces Bearer token auth when a registry verifier is configured, derives the reader identity from verified token claims, and returns the mention feed from service.a2a_mentions_feed().

taosmd/http_server.py

mentions.pyIntroduce SQLite-backed MentionStore for @handle indexing +85/-0

Introduce SQLite-backed MentionStore for @handle indexing

• Adds a new MentionStore with a mentions table + index keyed by mentioned_handle and ts. Provides APIs to record extracted @mentions (plus explicit recipient) and to query mentioned message IDs and per-message recipients.

taosmd/mentions.py

remote.pyExpose RemoteClient.a2a_mentions_feed() +20/-0

Expose RemoteClient.a2a_mentions_feed()

• Adds a remote client method that calls GET /a2a/mentions with since/limit and returns the server’s messages list. Documents that reader identity is derived server-side from registry auth token claims.

taosmd/remote.py

service.pyRecord mentions on send and implement mention-thread feed logic +196/-1

Record mentions on send and implement mention-thread feed logic

• Extends a2a_send() with an optional recipient field, stores it in the archived payload/receipt, and records body/recipient mentions into MentionStore. Adds a2a_mentions_feed() to return messages mentioning a reader plus their reply_to chains with thread_root metadata, and exports can_read() as a future thread-scoped visibility guard (currently short-circuited pending channel ACL enforcement).

taosmd/service.py

Tests (1) +512 / -0
test_a2a_mentions.pyAdd service- and HTTP-level tests for mentions feed, reply chains, and auth +512/-0

Add service- and HTTP-level tests for mentions feed, reply chains, and auth

• Introduces comprehensive tests covering mention extraction/indexing, mention feed semantics (since/limit, reply chain inclusion, sibling exclusion), and HTTP auth behavior for /a2a/mentions (401 unauthenticated, reader derived from token sub). Includes a standalone-mode test path using ?reader= when no registry verifier is configured.

tests/test_a2a_mentions.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (5)
taosmd/mentions.py (2)

25-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align init with the other store classes.

Two differences from the peer stores:

  1. ClaimStore.init, TemporalKnowledgeGraph.init, and ArchiveStore.init create the parent directory before connecting. MentionStore.init does not. Today _ensure_stores creates the data dir first, so this works. Any other caller that constructs MentionStore directly gets a connect failure.
  2. The peer stores call migrations.migrate(self._conn, "<name>") after executescript. MentionStore has no migration namespace, so a later change to the mentions schema has no upgrade path for existing a2a-mentions.db files.

Item 1 is a one-line fix. Item 2 is worth deciding now, before the table ships.

♻️ Proposed fix for item 1
+from pathlib import Path
+
     async def init(self) -> None:
+        Path(self._db_path).parent.mkdir(parents=True, exist_ok=True)
         self._conn = _db.connect(self._db_path)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@taosmd/mentions.py` around lines 25 - 38, Update MentionStore.init to create
the parent directory before calling _db.connect, matching ClaimStore.init,
TemporalKnowledgeGraph.init, and ArchiveStore.init. Also add a
migrations.migrate call after the mentions executescript using a dedicated,
stable migration namespace for this store.

59-65: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add a uniqueness constraint to match the documented key.

The class docstring declares the index is keyed by (mentioned_handle, message_id, ts, thread), but the schema has no UNIQUE constraint. A second record_mentions call for the same message_id inserts duplicate rows. get_mentioned_message_ids then returns the same message_id more than once, and the duplicates consume the LIMIT, so a2a_mentions_feed returns fewer distinct messages than limit requests.

a2a_send calls record_mentions once per message today, so this is a latent risk. A UNIQUE index plus INSERT OR IGNORE makes the write idempotent.

♻️ Proposed fix
             CREATE INDEX IF NOT EXISTS idx_mentions_handle_ts
                 ON mentions(mentioned_handle, ts);
+            CREATE UNIQUE INDEX IF NOT EXISTS idx_mentions_unique
+                ON mentions(mentioned_handle, message_id);
         for handle in handles:
             self._conn.execute(
-                "INSERT INTO mentions (mentioned_handle, message_id, ts, thread) VALUES (?, ?, ?, ?)",
+                "INSERT OR IGNORE INTO mentions "
+                "(mentioned_handle, message_id, ts, thread) VALUES (?, ?, ?, ?)",
                 (handle, message_id, ts, thread),
             )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@taosmd/mentions.py` around lines 59 - 65, Add a UNIQUE constraint or unique
index for the documented key `(mentioned_handle, message_id, ts, thread)` in the
mentions schema, then update `record_mentions` to use conflict-safe insertion
such as INSERT OR IGNORE. Preserve the existing commit behavior while making
repeated calls idempotent and preventing duplicate IDs from limiting
`get_mentioned_message_ids` results.
tests/test_a2a_mentions.py (2)

85-91: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for an expired or malformed bearer token.

_make_token never sets an exp claim. The suite covers three auth states on /a2a/mentions: no token (line 401), a valid token for the reader (line 406), and a valid token for a different subject (line 419).

It does not cover an expired token or a token signed by an unknown key. _handle_a2a_mentions in taosmd/http_server.py maps _ra.AuthError to 403 and a missing token to 401, and that 403 branch has no test. The docstring at lines 3-8 claims the file covers acceptance criterion 4 for authentication, so the gap matters.

Add an exp parameter to _make_token and assert 403 for an expired token.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_a2a_mentions.py` around lines 85 - 91, Add an optional exp
parameter to _make_token and include it in the JWT claims when provided, then
extend the /a2a/mentions authentication tests with an expired-token request and
assert it returns 403, covering the _handle_a2a_mentions AuthError path while
preserving existing token cases.

309-322: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The since cursor test depends on wall-clock sleeps.

The test writes a message, sleeps 20 ms, takes pivot = time.time(), sleeps 20 ms, then writes a second message. It then asserts that only the second message appears.

The mention ts comes from the archive row timestamp, which a2a_send reads back at line 420 of taosmd/service.py. On a loaded CI runner the two a2a_send calls can take longer than the 20 ms margin, and clock resolution differences can place a timestamp on the wrong side of pivot. The test then fails intermittently.

Read the recorded timestamps and derive the pivot from them instead of from time.time().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_a2a_mentions.py` around lines 309 - 322, Update
test_a2a_mentions_feed_since_cursor to derive pivot from the recorded archive
timestamps of the first message rather than time.time() and wall-clock sleeps.
Capture the timestamp associated with the “old `@bob`” send, then use a cursor
value between that timestamp and the second message’s timestamp when calling
a2a_mentions_feed, preserving the assertions that only “new `@bob`” is returned.
taosmd/service.py (1)

419-423: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce the extra archive read and align the store guard.

Two points:

  1. Line 420 re-reads the row that line 405 just wrote, only to recover timestamp. That is a second archive round trip on every send. If archive.record can return the timestamp, or if the caller can generate ts = time.time() before the write and pass it to record, the read disappears.
  2. Line 422-423 uses stores.get("mentions") with an isinstance guard, so a missing or wrong-typed store silently skips indexing. a2a_mentions_feed at line 676 uses stores["mentions"] and raises KeyError in the same situation. Pick one behaviour. Silent skip on the write path plus a hard failure on the read path is the worst combination: mentions go unindexed with no signal.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@taosmd/service.py` around lines 419 - 423, Remove the extra archive.get_event
read in the send flow around archive.record by generating or obtaining the
timestamp before the write and reusing it, or by consuming a timestamp returned
from record. Align the mentions store access in this indexing path with
a2a_mentions_feed: use the same strict missing/wrong-store behavior instead of
silently skipping when the store is absent or has the wrong type.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@taosmd/http_server.py`:
- Around line 1548-1591: Update _handle_a2a_mentions to archive every request
and outcome with append-only Archive.record(...): record the incoming
query/request before validation or retrieval, authentication and validation
failures before returning or raising, and the retrieved response after a
successful feed call. Include user input, assistant output, tool/error details
as applicable, and do not delete or overwrite archived data.

In `@taosmd/mentions.py`:
- Line 15: Update the _MENTION_RE pattern to require either the start of the
string or a character that cannot be part of a handle immediately before @,
while preserving the existing handle characters and extraction behavior.
- Around line 67-78: Update MentionStore.get_mentioned_message_ids to clamp the
incoming limit to the supported non-negative maximum before adding it to the SQL
parameters, ensuring negative and excessively large values remain bounded while
preserving the existing query behavior.

In `@taosmd/remote.py`:
- Around line 251-269: Update a2a_mentions_feed to forward its reader argument
in the GET /a2a/mentions request parameters, while preserving the existing limit
and optional since parameters so standalone servers can authenticate the request
without a registry verifier.

In `@taosmd/service.py`:
- Around line 739-740: Remove the final result truncation after result.sort in
the mentions feed, keeping limit solely as the cap on mention rows applied
earlier in the flow. Return all messages produced by reply-chain expansion so a
requested mention page does not split an expanded thread.
- Around line 687-715: Refactor the A2A mention-chain logic in
_handle_a2a_mentions to avoid loading and rescanning the entire archive: narrow
archive.query using the mention rows when possible, parse each row once into a
reply_to-to-children map, and traverse that map from mentioned_ids in memory.
Derive each message’s thread root from the same parent/child relationships
instead of calling _find_thread_root or archive.get_event per hop, while
preserving the existing reply-chain and root results.
- Around line 779-785: The can_read path is hardcoded to always allow access,
leaving the mentionGrant logic unreachable. In taosmd/service.py lines 779-785,
replace the if True short-circuit with a named module constant such as
_CHANNEL_ACL_ALWAYS_ALLOWS and move _api._ensure_stores below that check so
stores are loaded only when needed. In tests/test_a2a_mentions.py lines 358-382,
add coverage with the constant disabled that asserts mentionGrant allows a
reader mentioned on the thread root and denies one absent from the chain; retain
the existing always-allow tests.
- Around line 719-740: Update a2a_mentions_feed to apply the same A2AAdminState
filtering as a2a_feed before appending messages: exclude rows whose IDs are in
_superseded and rows belonging to channels in _deleted, while preserving the
existing admin_action suppression. Reuse the established admin-state symbols and
filtering behavior so the mentions feed does not bypass administrative controls.
- Line 354: Validate recipient in a2a_send alongside sender and body, rejecting
non-string values before archive.record runs so invalid requests cannot
partially persist. Update the Returns documentation for a2a_send to include
recipient among the conditionally returned receipt fields.

In `@tests/test_a2a_mentions.py`:
- Around line 419-427: Update test_http_mentions_authenticated_as_other_excludes
to post a second message mentioning `@alice` after the existing `@bob` message, then
assert alice’s mentions response contains the `@alice` message and excludes the
`@bob` message. Keep the authenticated request and existing status validation
unchanged so the test verifies filtering against a non-empty feed.
- Around line 53-59: Update the store cleanup loop to iterate over every store
in each cached store mapping, including the claims store created by
_ensure_stores, rather than selecting named keys. Remove the broad exception
suppression around store.close() so close failures propagate or are reported
according to the test fixture’s existing cleanup behavior.
- Around line 401-403: Update test_http_mentions_unauthenticated_returns_401 to
assert the returned 401 error payload using body, verifying the handler’s
expected unauthenticated message. Keep the status assertion and remove the
unused-variable issue by making the payload assertion part of the test.

---

Nitpick comments:
In `@taosmd/mentions.py`:
- Around line 25-38: Update MentionStore.init to create the parent directory
before calling _db.connect, matching ClaimStore.init,
TemporalKnowledgeGraph.init, and ArchiveStore.init. Also add a
migrations.migrate call after the mentions executescript using a dedicated,
stable migration namespace for this store.
- Around line 59-65: Add a UNIQUE constraint or unique index for the documented
key `(mentioned_handle, message_id, ts, thread)` in the mentions schema, then
update `record_mentions` to use conflict-safe insertion such as INSERT OR
IGNORE. Preserve the existing commit behavior while making repeated calls
idempotent and preventing duplicate IDs from limiting
`get_mentioned_message_ids` results.

In `@taosmd/service.py`:
- Around line 419-423: Remove the extra archive.get_event read in the send flow
around archive.record by generating or obtaining the timestamp before the write
and reusing it, or by consuming a timestamp returned from record. Align the
mentions store access in this indexing path with a2a_mentions_feed: use the same
strict missing/wrong-store behavior instead of silently skipping when the store
is absent or has the wrong type.

In `@tests/test_a2a_mentions.py`:
- Around line 85-91: Add an optional exp parameter to _make_token and include it
in the JWT claims when provided, then extend the /a2a/mentions authentication
tests with an expired-token request and assert it returns 403, covering the
_handle_a2a_mentions AuthError path while preserving existing token cases.
- Around line 309-322: Update test_a2a_mentions_feed_since_cursor to derive
pivot from the recorded archive timestamps of the first message rather than
time.time() and wall-clock sleeps. Capture the timestamp associated with the
“old `@bob`” send, then use a cursor value between that timestamp and the second
message’s timestamp when calling a2a_mentions_feed, preserving the assertions
that only “new `@bob`” is returned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 91c80ef4-f6c8-43cd-87e4-52726953b160

📥 Commits

Reviewing files that changed from the base of the PR and between dac15f0 and b8b5f7a.

📒 Files selected for processing (6)
  • taosmd/api.py
  • taosmd/http_server.py
  • taosmd/mentions.py
  • taosmd/remote.py
  • taosmd/service.py
  • tests/test_a2a_mentions.py

Comment thread taosmd/http_server.py
Comment on lines +1548 to +1591
def _handle_a2a_mentions(self, qs: dict) -> None:
since_raw = (qs.get("since") or [None])[0]
limit_raw = (qs.get("limit") or [50])[0]
try:
since = float(since_raw) if since_raw is not None else None
except (TypeError, ValueError) as exc:
raise _BadRequest("'since' must be a float timestamp") from exc
try:
limit_i = int(limit_raw)
except (TypeError, ValueError) as exc:
raise _BadRequest("'limit' must be an integer") from exc
# Auth: when a registry verifier is configured, the caller's
# verified identity is the reader. Unauthenticated requests
# return 401. When no verifier is configured (standalone), a
# ?reader= query parameter is accepted for testing.
if _registry_verifier is not None:
auth = self.headers.get("Authorization", "")
token = auth[len("Bearer "):].strip() if auth.startswith("Bearer ") else ""
if not token:
self._send_json(401, {"error": "registry auth: Bearer token required"})
return
try:
from . import registry_auth as _ra # noqa: PLC0415
import jwt as _jwt # noqa: PLC0415
unverified = _jwt.decode(token, options={"verify_signature": False})
raw_sub = unverified.get("sub", "") or ""
except Exception: # noqa: BLE001
raw_sub = ""
try:
claims = _registry_verifier.authorize(token, raw_sub)
reader = claims.get("sub", "")
except _ra.AuthError as exc:
self._send_json(403, {"error": f"registry auth: {exc}"})
return
else:
reader = (qs.get("reader") or [None])[0]
if not reader:
raise _BadRequest(
"'reader' query parameter is required when no registry verifier is configured"
)
messages = runner.run(
service.a2a_mentions_feed(reader, since=since, limit=limit_i, data_dir=data_dir)
)
self._send_json(200, {"messages": messages})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Archive mention-feed requests and outcomes.

Record the request before retrieval. Record the response after retrieval. Record authentication and validation failures before returning. Use append-only Archive.record(...) and do not delete archived data.

As per coding guidelines, archive every conversation turn, including user messages, assistant responses, tool calls, and errors, using taOSmd’s append-only Archive.record(...); archived data must not be deleted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@taosmd/http_server.py` around lines 1548 - 1591, Update _handle_a2a_mentions
to archive every request and outcome with append-only Archive.record(...):
record the incoming query/request before validation or retrieval, authentication
and validation failures before returning or raising, and the retrieved response
after a successful feed call. Include user input, assistant output, tool/error
details as applicable, and do not delete or overwrite archived data.

Source: Coding guidelines

Comment thread taosmd/mentions.py

from taosmd import _db

_MENTION_RE = re.compile(r'@([a-zA-Z0-9_-]+)')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Add a left boundary to the mention pattern.

_MENTION_RE matches @handle at any position. It also matches inside an email address or a URL. For example, mail bob@alice.org records a mention for alice, and see http://x/@carol records a mention for carol. A mention is an access grant here: a2a_mentions_feed returns the message and its reply chain to the mentioned handle. So a stray @ in a body can expose a thread to an unintended agent.

Require the start of the string or a non-handle character before @.

🛡️ Proposed fix
-_MENTION_RE = re.compile(r'@([a-zA-Z0-9_-]+)')
+# Require start-of-string or whitespace/punctuation before "@" so that
+# emails ("bob@alice.org") and URLs do not create mention grants.
+_MENTION_RE = re.compile(r'(?:(?<=\s)|(?<=^))@([a-zA-Z0-9_-]+)', re.MULTILINE)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@taosmd/mentions.py` at line 15, Update the _MENTION_RE pattern to require
either the start of the string or a character that cannot be part of a handle
immediately before @, while preserving the existing handle characters and
extraction behavior.

Comment thread taosmd/mentions.py
Comment on lines +67 to +78
async def get_mentioned_message_ids(
self, reader: str, since: float | None = None, limit: int = 50
) -> list[dict]:
query = "SELECT message_id, ts FROM mentions WHERE mentioned_handle = ?"
params: list = [reader]
if since is not None:
query += " AND ts > ?"
params.append(since)
query += " ORDER BY ts ASC LIMIT ?"
params.append(limit)
rows = self._conn.execute(query, params).fetchall()
return [{"message_id": r[0], "ts": r[1]} for r in rows]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find every caller of get_mentioned_message_ids and the limit values they pass.
rg -nP -C 5 '\bget_mentioned_message_ids\s*\(' --type=py
# Check how the HTTP layer validates the limit query parameter.
rg -nP -C 6 'limit_raw|_handle_a2a_mentions' --type=py

Repository: jaylfc/taosmd

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -u

echo "Tracked files matching mentions/http:"
git ls-files | rg '(^|/)mentions\.py$|(^|/)http_server\.py$|(^|/)a2a_feed\.py$' || true

echo
echo "Find get_mentioned_message_ids with fixed string (no type filter):"
rg -n -C 5 'get_mentioned_message_ids' . || true

echo
echo "Find HTTP mention handlers (no type filter):"
rg -n -C 6 'handle.*mention|a2a/mentions|limit_raw|_handle_a2a_mentions|mentions' . || true

Repository: jaylfc/taosmd

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -u

echo "taosmd/mentions.py:"
cat -n taosmd/mentions.py

echo
echo "taosmd/http_server.py around mention endpoints and route parsing:"
rg -n -C 8 'a2a/mentions|handle_a2a|http.route|route\(|query\(|limit' taosmd/http_server.py taosmd/*.py | sed -n '1,260p'

echo
echo "service.py a2a_mentions_feed block:"
sed -n '651,705p' taosmd/service.py | cat -n

echo
echo "remote.py a2a_mentions_feed block:"
sed -n '251,269p' taosmd/remote.py | cat -n

echo
echo "Tests around a2a_mentions_feed and limit:"
rg -n -C 5 'a2a_mentions_feed|limit|mentions' tests/test_a2a_mentions.py taosmd/tests 2>/dev/null || true

Repository: jaylfc/taosmd

Length of output: 49562


🏁 Script executed:

#!/bin/bash
set -u

echo "taosmd/http_server.py size and relevant mentions route:"
wc -l taosmd/http_server.py
rg -n -C 10 '/a2a/mentions|make_server|query_string|parse|service_loop|GET|routes' taosmd/http_server.py

echo
echo "Focused sections around line numbers:"
sed -n '1,220p' taosmd/http_server.py | cat -n

Repository: jaylfc/taosmd

Length of output: 47982


🏁 Script executed:

#!/bin/bash
set -u

echo "_handle_a2a_mentions implementation:"
rg -n -C 35 'def _handle_a2a_mentions' taosmd/http_server.py

echo
echo "Parse-query helper locations:"
rg -n -C 5 'def .*parse|parse_qs|qs\.get\(.*since|qs\.get\(.*limit|limit_raw|since_raw' taosmd/http_server.py

Repository: jaylfc/taosmd

Length of output: 12421


🏁 Script executed:

#!/bin/bash
set -u

echo "_handle_a2a_mentions remainder:"
sed -n '1548,1598p' taosmd/http_server.py | cat -n

echo
echo "service.a2a_mentions_feed remainder:"
sed -n '699,735p' taosmd/service.py | cat -n

Repository: jaylfc/taosmd

Length of output: 4860


Clamp limit at the store boundary.

get_mentioned_message_ids passes limit directly into LIMIT ?, and the HTTP handler only parses ?limit= as an integer. A negative LIMIT disables the limit, and a large positive value removes the intended cap. Clamp limit to the supported non-negative range in MentionStore so each call remains bounded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@taosmd/mentions.py` around lines 67 - 78, Update
MentionStore.get_mentioned_message_ids to clamp the incoming limit to the
supported non-negative maximum before adding it to the SQL parameters, ensuring
negative and excessively large values remain bounded while preserving the
existing query behavior.

Comment thread taosmd/remote.py
Comment on lines +251 to +269
async def a2a_mentions_feed(
self,
reader: str,
*,
since: float | None = None,
limit: int = 50,
**_opts,
) -> list[dict]:
"""GET /a2a/mentions: return messages mentioning ``reader`` plus reply chains.

Returns the ``messages`` list from the server response. Requires
registry auth on the server side; the ``reader`` identity is derived
from the verified token ``sub``.
"""
params: dict = {"limit": limit}
if since is not None:
params["since"] = since
resp = await self._run("GET", "/a2a/mentions", params=params)
return resp.get("messages", [])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Forward reader for standalone servers.

Line 253 accepts reader, but Line 268 drops it. When no registry verifier is configured, taosmd/http_server.py requires ?reader=. Every remote mention-feed request to a standalone server returns 400.

Proposed fix
-        params: dict = {"limit": limit}
+        params: dict = {"reader": reader, "limit": limit}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def a2a_mentions_feed(
self,
reader: str,
*,
since: float | None = None,
limit: int = 50,
**_opts,
) -> list[dict]:
"""GET /a2a/mentions: return messages mentioning ``reader`` plus reply chains.
Returns the ``messages`` list from the server response. Requires
registry auth on the server side; the ``reader`` identity is derived
from the verified token ``sub``.
"""
params: dict = {"limit": limit}
if since is not None:
params["since"] = since
resp = await self._run("GET", "/a2a/mentions", params=params)
return resp.get("messages", [])
async def a2a_mentions_feed(
self,
reader: str,
*,
since: float | None = None,
limit: int = 50,
**_opts,
) -> list[dict]:
"""GET /a2a/mentions: return messages mentioning ``reader`` plus reply chains.
Returns the ``messages`` list from the server response. Requires
registry auth on the server side; the ``reader`` identity is derived
from the verified token ``sub``.
"""
params: dict = {"reader": reader, "limit": limit}
if since is not None:
params["since"] = since
resp = await self._run("GET", "/a2a/mentions", params=params)
return resp.get("messages", [])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@taosmd/remote.py` around lines 251 - 269, Update a2a_mentions_feed to forward
its reader argument in the GET /a2a/mentions request parameters, while
preserving the existing limit and optional since parameters so standalone
servers can authenticate the request without a registry verifier.

Comment thread taosmd/service.py
reply_to: str | None = None,
refs: list | None = None,
blocks: list | None = None,
recipient: str | None = None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate recipient like sender and body.

a2a_send validates sender and body as non-empty strings at lines 380-383. recipient gets no check. The HTTP /a2a/send handler forwards JSON body values, so recipient can arrive as a list, a dict, or a number.

The failure is order-dependent. archive.record at line 405 runs first and succeeds. Then record_mentions calls handles.add(recipient). For an unhashable value such as a list this raises TypeError. The request returns an error, but the message is already in the append-only archive without a mention row. The archive and the mention index then disagree.

Reject a non-string recipient up front.

🛡️ Proposed fix
     if not isinstance(body, str) or not body:
         raise ValueError("body must be a non-empty string")
+    if recipient is not None and (not isinstance(recipient, str) or not recipient):
+        raise ValueError("recipient must be a non-empty string when provided")

Also update the "Returns" paragraph at lines 374-375. It lists refs and blocks as the conditional receipt fields but omits recipient, which lines 413-414 now add.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@taosmd/service.py` at line 354, Validate recipient in a2a_send alongside
sender and body, rejecting non-string values before archive.record runs so
invalid requests cannot partially persist. Update the Returns documentation for
a2a_send to include recipient among the conditionally returned receipt fields.

Comment thread taosmd/service.py
Comment on lines +739 to +740
result.sort(key=lambda m: m["ts"])
return result[:limit]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

limit is applied twice with different meanings.

Line 679 uses limit to cap the number of mention rows. Line 740 uses the same limit to cap the final result, after the reply chain expanded the set.

The two caps interact badly. Consider limit=3 with one mentioned root that has five replies. Line 679 returns one mention row. The expansion adds five replies, giving six messages. Line 739 sorts by ts, and line 740 keeps the three oldest. A reader who asks for a page of mentions gets a partial thread with no cursor to continue from.

test_a2a_mentions_feed_limit asserts only len(msgs) == 3, so it passes either way.

Decide which one limit means. If it caps mentions, drop the truncation at line 740. If it caps returned messages, document that a thread can be split across pages and return a cursor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@taosmd/service.py` around lines 739 - 740, Remove the final result truncation
after result.sort in the mentions feed, keeping limit solely as the cap on
mention rows applied earlier in the flow. Return all messages produced by
reply-chain expansion so a requested mention page does not split an expanded
thread.

Comment thread taosmd/service.py
Comment on lines +779 to +785
stores = await _api._ensure_stores(data_dir)
archive = stores["archive"]
mentions_store = stores["mentions"]

# channelACL: no enforcement yet (tsk-dp6fyv); always passes.
if True:
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

can_read is stubbed to always return True, so both of its tests pass without exercising any logic. The if True: short-circuit at taosmd/service.py line 784 makes lines 787-819 unreachable. Every can_read call returns True. The two tests then assert True for the granted case and for the non-granted case, so neither can fail and neither covers the mentionGrant walk.

  • taosmd/service.py#L779-L785: replace if True: with a named module constant such as _CHANNEL_ACL_ALWAYS_ALLOWS, and move the _ensure_stores call below the short-circuit so the unused archive and mentions_store loads disappear.
  • tests/test_a2a_mentions.py#L358-L382: add a test that sets the constant to False and drives the mentionGrant walk directly. Assert True for a reader mentioned on the thread root and False for a reader mentioned nowhere in the chain. Keep the existing two tests as the always-allow baseline.
📍 Affects 2 files
  • taosmd/service.py#L779-L785 (this comment)
  • tests/test_a2a_mentions.py#L358-L382
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@taosmd/service.py` around lines 779 - 785, The can_read path is hardcoded to
always allow access, leaving the mentionGrant logic unreachable. In
taosmd/service.py lines 779-785, replace the if True short-circuit with a named
module constant such as _CHANNEL_ACL_ALWAYS_ALLOWS and move _api._ensure_stores
below that check so stores are loaded only when needed. In
tests/test_a2a_mentions.py lines 358-382, add coverage with the constant
disabled that asserts mentionGrant allows a reader mentioned on the thread root
and denies one absent from the chain; retain the existing always-allow tests.

Comment on lines +53 to +59
for stores in list(taosmd_api._stores_cache.values()):
for store in (stores.get("archive"), stores.get("vector"), stores.get("kg"), stores.get("mentions")):
if store and hasattr(store, "close"):
try:
asyncio.run(store.close())
except Exception:
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Close the claims store too, and do not swallow close errors silently.

Two points:

  1. Line 54 closes archive, vector, kg, and mentions. _ensure_stores in taosmd/api.py also creates a ClaimStore at line 145 and holds its SQLite connection. The fixture never closes it, so each test in this file leaks one open handle against a tmp_path database.
  2. Lines 56-59 catch every exception and pass. Ruff flags this as S110 and BLE001. A failed close() produces no signal, so a leak or a lock error stays invisible.

Iterate the store values instead of naming a subset. That way a new store added to _ensure_stores is closed automatically.

♻️ Proposed fix
     for stores in list(taosmd_api._stores_cache.values()):
-        for store in (stores.get("archive"), stores.get("vector"), stores.get("kg"), stores.get("mentions")):
-            if store and hasattr(store, "close"):
-                try:
-                    asyncio.run(store.close())
-                except Exception:
-                    pass
+        for name, store in stores.items():
+            if not hasattr(store, "close"):
+                continue
+            try:
+                asyncio.run(store.close())
+            except OSError as exc:  # noqa: PERF203
+                print(f"failed to close store {name}: {exc}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for stores in list(taosmd_api._stores_cache.values()):
for store in (stores.get("archive"), stores.get("vector"), stores.get("kg"), stores.get("mentions")):
if store and hasattr(store, "close"):
try:
asyncio.run(store.close())
except Exception:
pass
for stores in list(taosmd_api._stores_cache.values()):
for name, store in stores.items():
if not hasattr(store, "close"):
continue
try:
asyncio.run(store.close())
except OSError as exc: # noqa: PERF203
print(f"failed to close store {name}: {exc}")
🧰 Tools
🪛 Ruff (0.16.0)

[error] 58-59: try-except-pass detected, consider logging the exception

(S110)


[warning] 58-58: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_a2a_mentions.py` around lines 53 - 59, Update the store cleanup
loop to iterate over every store in each cached store mapping, including the
claims store created by _ensure_stores, rather than selecting named keys. Remove
the broad exception suppression around store.close() so close failures propagate
or are reported according to the test fixture’s existing cleanup behavior.

Source: Linters/SAST tools

Comment on lines +401 to +403
def test_http_mentions_unauthenticated_returns_401(authed_server):
status, body = _get(f"{authed_server}/a2a/mentions")
assert status == 401

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert the 401 error payload and remove the unused variable.

Line 402 unpacks body and never uses it. Ruff reports this as RUF059. The handler returns a specific message for the unauthenticated case, so asserting it costs one line and makes the test verify the contract rather than only the status code.

💚 Proposed fix
 def test_http_mentions_unauthenticated_returns_401(authed_server):
     status, body = _get(f"{authed_server}/a2a/mentions")
-    assert status == 401
+    assert status == 401, body
+    assert "Bearer token required" in body["error"]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_http_mentions_unauthenticated_returns_401(authed_server):
status, body = _get(f"{authed_server}/a2a/mentions")
assert status == 401
def test_http_mentions_unauthenticated_returns_401(authed_server):
status, body = _get(f"{authed_server}/a2a/mentions")
assert status == 401, body
assert "Bearer token required" in body["error"]
🧰 Tools
🪛 Ruff (0.16.0)

[warning] 402-402: Unpacked variable body is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_a2a_mentions.py` around lines 401 - 403, Update
test_http_mentions_unauthenticated_returns_401 to assert the returned 401 error
payload using body, verifying the handler’s expected unauthenticated message.
Keep the status assertion and remove the unused-variable issue by making the
payload assertion part of the test.

Source: Linters/SAST tools

Comment on lines +419 to +427
def test_http_mentions_authenticated_as_other_excludes(authed_server):
alice_token = _make_token("alice", iss=REGISTRY_ISS)
_post(f"{authed_server}/a2a/send",
{"from": "agentA", "body": "hey @bob", "thread": "cross"}, token=alice_token)

status, body = _get(f"{authed_server}/a2a/mentions", token=alice_token)
assert status == 200, body
msgs = body["messages"]
assert all("hey @bob" not in m.get("body", "") for m in msgs)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

This test passes trivially and does not prove exclusion.

alice posts a message that mentions @bob. alice then reads her own mention feed. alice is never mentioned anywhere in this test, so her feed is empty. The assertion at line 427 holds over an empty list regardless of whether the filter works.

Make the test meaningful: mention @alice in a second message, then assert her feed contains that message and not the @bob one. The feed then has content, and the assertion tests the handle filter.

💚 Proposed fix
 def test_http_mentions_authenticated_as_other_excludes(authed_server):
     alice_token = _make_token("alice", iss=REGISTRY_ISS)
     _post(f"{authed_server}/a2a/send",
           {"from": "agentA", "body": "hey `@bob`", "thread": "cross"}, token=alice_token)
+    _post(f"{authed_server}/a2a/send",
+          {"from": "agentA", "body": "hey `@alice`", "thread": "cross"}, token=alice_token)
 
     status, body = _get(f"{authed_server}/a2a/mentions", token=alice_token)
     assert status == 200, body
     msgs = body["messages"]
+    bodies = [m["body"] for m in msgs]
+    assert "hey `@alice`" in bodies
-    assert all("hey `@bob`" not in m.get("body", "") for m in msgs)
+    assert "hey `@bob`" not in bodies
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_http_mentions_authenticated_as_other_excludes(authed_server):
alice_token = _make_token("alice", iss=REGISTRY_ISS)
_post(f"{authed_server}/a2a/send",
{"from": "agentA", "body": "hey @bob", "thread": "cross"}, token=alice_token)
status, body = _get(f"{authed_server}/a2a/mentions", token=alice_token)
assert status == 200, body
msgs = body["messages"]
assert all("hey @bob" not in m.get("body", "") for m in msgs)
def test_http_mentions_authenticated_as_other_excludes(authed_server):
alice_token = _make_token("alice", iss=REGISTRY_ISS)
_post(f"{authed_server}/a2a/send",
{"from": "agentA", "body": "hey `@bob`", "thread": "cross"}, token=alice_token)
_post(f"{authed_server}/a2a/send",
{"from": "agentA", "body": "hey `@alice`", "thread": "cross"}, token=alice_token)
status, body = _get(f"{authed_server}/a2a/mentions", token=alice_token)
assert status == 200, body
msgs = body["messages"]
bodies = [m["body"] for m in msgs]
assert "hey `@alice`" in bodies
assert "hey `@bob`" not in bodies
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_a2a_mentions.py` around lines 419 - 427, Update
test_http_mentions_authenticated_as_other_excludes to post a second message
mentioning `@alice` after the existing `@bob` message, then assert alice’s mentions
response contains the `@alice` message and excludes the `@bob` message. Keep the
authenticated request and existing status validation unchanged so the test
verifies filtering against a non-empty feed.

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 13 rules

Grey Divider


Action required

1. Mentions feed drops old 🐞 Bug ≡ Correctness
Description
service.a2a_mentions_feed only scans the most recent 100,000 A2A archive rows, so mentions that
point to older message_ids will never be returned and reply chains can be incomplete on long-running
installs. This makes /a2a/mentions silently incorrect once the A2A archive exceeds that window.
Code

taosmd/service.py[R687-688]

+    all_rows = await archive.query(event_type=EVENT_A2A, limit=100_000)
+    reply_chain_ids = set(mentioned_ids)
Relevance

●● Moderate

Fixing 100k scan cap is correctness vs intentional bounded-work tradeoff; no close precedent on
archive window limits.

PR-#195

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The feed builds visibility and the response strictly from all_rows, but all_rows is limited to
the newest 100k rows; archive.query is ordered newest-first with LIMIT/OFFSET, so older mentioned
messages (still present in the mention index) can be absent from the scan and thus omitted from
output.

taosmd/service.py[651-741]
taosmd/archive.py[287-328]
taosmd/mentions.py[67-78]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`a2a_mentions_feed()` limits its archive scan to `limit=100_000`, which makes the mentions endpoint incomplete when the archive contains more than 100k A2A rows.

### Issue Context
The mention index (`MentionStore`) can return `message_id`s that are older than the newest 100k archive rows; the feed then only emits rows present in the truncated `all_rows` list.

### Fix Focus Areas
- taosmd/service.py[651-741]
- taosmd/archive.py[287-328]
- taosmd/mentions.py[67-78]

### Suggested fix
- Remove the fixed `100_000` cap and instead **page** through `archive.query(..., limit=batch, offset=...)` until you’ve loaded enough history to include the oldest relevant mention timestamp (e.g., `min(r["ts"])` from `mentioned_rows`) plus any additional rows needed to resolve reply chains.
- Alternatively, add an archive helper to fetch specific IDs (and potentially their descendants) so the endpoint doesn’t rely on a global “latest N rows” scan.
- Add a regression test that simulates an archive with >100k A2A events where a mention points to an older message and verify it is still returned.

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



Remediation recommended

2. Mentions page selects oldest 🐞 Bug ≡ Correctness
Description
MentionStore.get_mentioned_message_ids always uses ORDER BY ts ASC LIMIT ?, so when since is
omitted the first /a2a/mentions page returns the oldest mentions rather than the most recent ones.
Unlike a2a_feed (which explicitly limits the most-recent N when since is None), this makes it hard
for clients to efficiently fetch current mentions once a reader has more than limit mentions.
Code

taosmd/mentions.py[R75-76]

+        query += " ORDER BY ts ASC LIMIT ?"
+        params.append(limit)
Relevance

●● Moderate

Could be intended oldest-first (doc says so) vs expected newest-first; no close historical precedent
found.

PR-#195

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The mention-store SQL always orders ascending with a LIMIT regardless of whether since is set,
while the existing A2A messages feed explicitly limits the most recent N when since is None by
querying newest-first then reversing.

taosmd/mentions.py[67-78]
taosmd/service.py[434-498]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The mentions index query always returns the oldest rows first (`ORDER BY ts ASC LIMIT ?`), which makes the default initial page (`since=None`) return stale data once there are more than `limit` mentions.

### Issue Context
`a2a_feed()` documents/implements a different behavior: when `since` is `None`, it limits the most recent N rows (newest-first query then reverse to chronological).

### Fix Focus Areas
- taosmd/mentions.py[67-78]
- taosmd/service.py[434-498]

### Suggested fix
- Adjust `get_mentioned_message_ids()` to mirror `a2a_feed` semantics:
 - If `since is None`: `ORDER BY ts DESC LIMIT ?` (select most recent page) and reverse in Python before returning to preserve oldest-first output ordering.
 - If `since is not None`: keep `ORDER BY ts ASC LIMIT ?` (cursor-forward paging).
- Add a test where a reader has >limit mentions and verify that `since=None, limit=N` returns the most recent N (in ascending order for display).

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



Informational

3. Mentions feed slow path 🐞 Bug ➹ Performance
Description
a2a_mentions_feed repeatedly scans the entire loaded archive window to compute the reply-chain
fixpoint and then performs per-message DB lookups to compute thread roots; this can cause high
latency and block the single service-loop thread under load. The cost grows with the number of
scanned rows and reply-chain depth, and the thread-root phase adds additional DB round-trips per
visible message.
Code

taosmd/service.py[R689-692]

+    changed = True
+    while changed:
+        changed = False
+        for row in all_rows:
Relevance

● Weak

Team has rejected similar perf refactors (avoid scans / N+1 batching) as non-required optimizations.

PR-#195
PR-#190

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The implementation loops over all_rows in a repeated fixpoint until stable, then calls
_find_thread_root() for each row in the visible set; _find_thread_root() calls
archive.get_event(), which executes a SQL SELECT per call.

taosmd/service.py[685-716]
taosmd/service.py[743-765]
taosmd/archive.py[330-343]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`a2a_mentions_feed()` does (1) a fixpoint loop that re-iterates `all_rows` until no new reply-chain IDs are found, and (2) a per-visible-message `_find_thread_root()` call that issues `archive.get_event()` DB queries.

### Issue Context
This runs on the single `_ServiceLoop` thread (HTTP server design), so expensive CPU/SQLite work here delays all other service calls.

### Fix Focus Areas
- taosmd/service.py[685-740]
- taosmd/service.py[743-765]
- taosmd/archive.py[330-343]

### Suggested fix
- Parse `data_json` **once** per row and build in-memory maps:
 - `reply_to_by_id: dict[int, int|None]`
 - `children_by_parent: dict[int, list[int]]` (reverse edges)
- Compute the visible set with a BFS/queue from `mentioned_ids` over `children_by_parent` (linear in scanned rows/edges, no fixpoint rescans).
- Compute `thread_root` with memoization using `reply_to_by_id` (no `archive.get_event()` calls for roots that are already in `all_rows`).
- If roots can be older than the scanned window, fall back to `archive.get_event()` only for those missing IDs (bounded), rather than for every visible row.

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


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread taosmd/service.py
Comment on lines +687 to +688
all_rows = await archive.query(event_type=EVENT_A2A, limit=100_000)
reply_chain_ids = set(mentioned_ids)

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

1. Mentions feed drops old 🐞 Bug ≡ Correctness

service.a2a_mentions_feed only scans the most recent 100,000 A2A archive rows, so mentions that
point to older message_ids will never be returned and reply chains can be incomplete on long-running
installs. This makes /a2a/mentions silently incorrect once the A2A archive exceeds that window.
Agent Prompt
### Issue description
`a2a_mentions_feed()` limits its archive scan to `limit=100_000`, which makes the mentions endpoint incomplete when the archive contains more than 100k A2A rows.

### Issue Context
The mention index (`MentionStore`) can return `message_id`s that are older than the newest 100k archive rows; the feed then only emits rows present in the truncated `all_rows` list.

### Fix Focus Areas
- taosmd/service.py[651-741]
- taosmd/archive.py[287-328]
- taosmd/mentions.py[67-78]

### Suggested fix
- Remove the fixed `100_000` cap and instead **page** through `archive.query(..., limit=batch, offset=...)` until you’ve loaded enough history to include the oldest relevant mention timestamp (e.g., `min(r["ts"])` from `mentioned_rows`) plus any additional rows needed to resolve reply chains.
- Alternatively, add an archive helper to fetch specific IDs (and potentially their descendants) so the endpoint doesn’t rely on a global “latest N rows” scan.
- Add a regression test that simulates an archive with >100k A2A events where a mention points to an older message and verify it is still returned.

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

Comment thread taosmd/mentions.py
Comment on lines +75 to +76
query += " ORDER BY ts ASC LIMIT ?"
params.append(limit)

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

3. Mentions page selects oldest 🐞 Bug ≡ Correctness

MentionStore.get_mentioned_message_ids always uses ORDER BY ts ASC LIMIT ?, so when since is
omitted the first /a2a/mentions page returns the oldest mentions rather than the most recent ones.
Unlike a2a_feed (which explicitly limits the most-recent N when since is None), this makes it hard
for clients to efficiently fetch current mentions once a reader has more than limit mentions.
Agent Prompt
### Issue description
The mentions index query always returns the oldest rows first (`ORDER BY ts ASC LIMIT ?`), which makes the default initial page (`since=None`) return stale data once there are more than `limit` mentions.

### Issue Context
`a2a_feed()` documents/implements a different behavior: when `since` is `None`, it limits the most recent N rows (newest-first query then reverse to chronological).

### Fix Focus Areas
- taosmd/mentions.py[67-78]
- taosmd/service.py[434-498]

### Suggested fix
- Adjust `get_mentioned_message_ids()` to mirror `a2a_feed` semantics:
  - If `since is None`: `ORDER BY ts DESC LIMIT ?` (select most recent page) and reverse in Python before returning to preserve oldest-first output ordering.
  - If `since is not None`: keep `ORDER BY ts ASC LIMIT ?` (cursor-forward paging).
- Add a test where a reader has >limit mentions and verify that `since=None, limit=N` returns the most recent N (in ascending order for display).

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

Comment thread taosmd/service.py
return await remote.a2a_send(
sender, body, thread=thread, reply_to=reply_to,
refs=refs, blocks=blocks,
refs=refs, blocks=blocks, recipient=recipient,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CRITICAL: remote.a2a_send swallows recipient via **_opts

service.a2a_send now passes recipient=recipient to remote.a2a_send, but RemoteClient.a2a_send accepts **_opts and silently drops the recipient kwarg without forwarding it to the server. Remote deployments lose the recipient field entirely, so the new mentions feature is broken for any caller using TAOSMD_SERVER_URL.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread taosmd/remote.py
params: dict = {"limit": limit}
if since is not None:
params["since"] = since
resp = await self._run("GET", "/a2a/mentions", params=params)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: a2a_mentions_feed accepts reader but never forwards it

RemoteClient.a2a_mentions_feed takes a reader parameter but does not include it in the GET request parameters. The server derives reader from the authenticated token's sub claim, so the parameter is silently ignored. This makes the client method signature misleading — callers may expect to query mentions for an arbitrary user, but they can only query for the token owner.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Aug 2, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 1
Issue Details (click to expand)

CRITICAL

File Line Issue
taosmd/service.py 388 remote.a2a_send swallows recipient via **_opts, breaking the feature for remote deployments

WARNING

File Line Issue
taosmd/remote.py 268 a2a_mentions_feed accepts reader but never forwards it to the server
Files Reviewed (6 files)
  • taosmd/service.py - 1 issue
  • taosmd/remote.py - 1 issue
  • taosmd/api.py
  • taosmd/http_server.py
  • taosmd/mentions.py
  • tests/test_a2a_mentions.py

Fix these issues in Kilo Cloud


Reviewed by step-3.7-flash · Input: 186.9K · Output: 34.3K · Cached: 2.1M

@jaylfc

jaylfc commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

FIX, and the good news first because it is the part that matters: the index is REAL. mentions.py has a proper (handle, ts) index and the query is a covered range scan, measured flat at 0.31-0.35ms whether the archive holds 10k or 50k rows. Crucially the early return fires BEFORE any scan, so the steady-state 'anything for me?' poll is 0.39ms flat. That is exactly the cheap-poll primitive the zero-token watcher needs, and it is delivered. Also: 21 tests, they run, and two of three mutations go red. Best-shaped taosmd PR today.

The cost mechanism is half-built though: free to ask, whole-archive to answer.

BLOCKING:

  1. SECURITY, the one I would fix first. The mention regex has no left boundary and a mention is an ACCESS GRANT: the feed hands the mentioned handle the message AND its whole reply chain, deliberately across channel boundaries. Measured false positives: jaylfc25@gmail.com grants to a handle 'gmail'; a fenced code block containing @Property grants to 'property'; a URL .../@taosc-dev/repo matches. Any agent registered under one of those names reads threads it was never in. Add a left boundary and skip fenced code, and reproduce the three cases before fixing.
  2. Case sensitivity, which breaks my own handle. Lookup is exact-match with no COLLATE NOCASE, so @taOS-dev does not resolve as taos-dev. Zero matches.
  3. recipient never leaves the process. service.a2a_send accepts it and forwards it, but remote.a2a_send has no such parameter so it is swallowed by **_opts with no error, and the HTTP handler never passes it at all. Since every agent on the Pi talks HTTP, the explicit-recipient path is reachable only from an in-process Python call. All the tests call service directly, which is why nothing caught it. Wire it through HTTP and remote, and test through HTTP.
  4. The feed loads the ENTIRE archive on every hit (archive.query limit=100_000), re-parses every row per fixpoint pass, and does one sequential get_event per chain hop. Measured 116ms at 10k rows and 498ms at 50k, against 91ms for just reading the channel it replaces. Fetch the mentioned rows by id and walk the chain with an indexed reply_to lookup instead. Also: past 100k A2A rows, mentions on older messages are silently absent from the feed, proven by capping the limit.
  5. The mention feed bypasses admin suppression. Reproduced end to end: supersede a message, a2a_feed returns nothing, /a2a/mentions still returns 'secret @bob leak'. Apply the same superseded and deleted filters a2a_feed uses.
  6. can_read is dead code. service.py has 'if True:' followed by return True, so 33 lines below are unreachable; deleting all of them left 21/21 green. Its two tests assert True is True, one of them named no_grant. Either wire it for real so that mutation goes red, or delete it and the tests.
  7. No backfill, and none planned. Every message predating deploy is permanently invisible to the feed, silently. Add a backfill with UNIQUE(mentioned_handle, message_id) so re-runs are idempotent, tested over a populated pre-change database.
  8. remote.py drops reader from the params, so every remote mention-feed call to a standalone server 400s.

Adjudication of the bots: CodeRabbit was largely right and found six of these independently. Its recipient finding has the wrong mechanism (it assumed HTTP wiring exists, which is itself evidence of blocker 3) but a useful conclusion. REJECT qodo's suggestion to flip ORDER BY ts ASC to DESC: ASC is correct and load-bearing, it is what makes the watermark gapless, and I verified exactly-once delivery over a 9-mention interleaved chain. Fix the cold-start concern with a documented cursor, never by reversing the sort. Kilo never completed here, so treat it as unadjudicated rather than clean.

NON-BLOCKING but worth doing while you are in there: return next_since so clients do not have to infer the watermark; tiebreak the cursor on (ts, id), since equal timestamps silently drop a row at a page boundary and a backfill with coarse timestamps would hit that immediately; clamp limit and reject negatives (a negative becomes LIMIT -1, unlimited); expose the feed on MCP alongside a2a_read.

Retitle please, seventh in a row wearing the uv.lock cleanup subject.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant