Skip to content

feat(team): add read-only mailbox/task activity API and real-time events - #740

Open
piorpua wants to merge 16 commits into
mainfrom
aionissue/task-20260729-001-001
Open

feat(team): add read-only mailbox/task activity API and real-time events#740
piorpua wants to merge 16 commits into
mainfrom
aionissue/task-20260729-001-001

Conversation

@piorpua

@piorpua piorpua commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the backend read APIs and real-time events that power a new read-only Message & Task activity view for Team mode. Each team member (including the leader) already has a mailbox and a task-board persisted server-side, but there was no user-facing way to read the whole team's mailbox/tasks, and the message/task change events were asymmetric. This PR fills that capability gap.

This is one half of a two-repo change and is consumed by the paired AionUi PR (branch aionissue/task-20260729-001-001). Both should be merged together.

Read-only scope: SELECT read paths plus in-memory mapping. No DB schema change, no migration, no backfill.

What changed

  • API types (aionui-api-types): new TeamMailboxMessageResponse, TeamTaskResponse, TeamMailboxChange / TeamTaskChange enums, TeamTaskChangedPayload / TeamMailboxChangedPayload, plus a unified paginated feed (TeamActivityKind, TeamActivityItemResponse, TeamActivityCursor, TeamActivityPageResponse). No axum/HTTP dependency added.
  • Repository layer (aionui-db): list_messages_by_team (DESC + clamped limit), list_messages_by_ids (batched/chunked IN), keyset-paginated list_messages_by_team_paged and list_tasks_paged, and list_tasks_by_ids for dependency-label resolution — added to the trait, the SQLite implementation, and mocks.
  • Domain layer (aionui-team): new activity_mapping (row/domain → response types; malformed files JSON degrades to empty with a warn); events gains a team.mailboxChanged constant plus broadcast_task_changed / broadcast_mailbox_changed, with payloads scoped per user (task/mailbox/slot-work events all narrowed to avoid cross-user leakage); mailbox / task-board emit events at their single write choke points via an optionally-injected emitter (no-op when absent); the emitter is injected at the sole production construction site.
  • Service layer: list_team_mailbox / list_team_tasks / list_team_activity, all gated through the existing owned-team load (team-not-found → 404, cross-user → Forbidden) with limit clamping.
  • HTTP routes: GET /api/teams/{id}/mailbox, GET /api/teams/{id}/tasks (supports ?ids= for dependency resolution), and GET /api/teams/{id}/activity (unified cursor-paginated feed with direction / kind / limit and fallback), reusing the existing team auth middleware.

Adjacent changes reviewers should note separately

  • Wake task owners on assignment / unblock.
  • Removal of a dead SchedulerAction dispatch path (related to tightening the single write path).

Layering / conventions

  • Event emission lives in aionui-team, not aionui-db; response types stay in aionui-api-types with no HTTP-framework dependency; route handlers only transform request/response, logic stays in the service.
  • New read endpoints enforce auth and cross-user isolation; WS payloads are user-scoped; responses expose no sensitive/internal metadata.

Testing

Local pre-push gate passed on the branch:

  • just migration-check — passed (migrations immutable; none added).
  • cargo fmt --all / lint-fix (cargo clippy --fix --workspace -- -D warnings) — clean, no changes.
  • cargo nextest run --workspace — 8016 passed, 22 skipped.

Covered: activity DTO/enum/payload serde; list_messages_by_team ordering/limit and list_messages_by_ids batching; activity_mapping; emitter wiring (mailbox / task-board) and event shapes; and read-endpoint service integration tests (happy / 404 / forbidden / no-metadata).

Not exercised here (needs manual/live verification with the frontend): end-to-end WS delivery over a real connection, cross-user WS isolation under live traffic, and the paginated feed under infinite scroll.

Notes

  • PR base is current main. The branch merged upstream release v0.1.56; that release is already contained in main, so this PR's diff is fix-only (no upstream migrations or release content leak into scope).

zynx added 16 commits July 29, 2026 15:01
Back the Team "message & task flow" view with read-only endpoints and two
symmetric WebSocket channels so the frontend can render every member's
mailbox and task board with live updates.

- api-types: add TeamMailboxMessageResponse, TeamTaskResponse (no metadata),
  TeamMailboxChange/TeamTaskChange enums, and the TeamMailboxChangedPayload /
  TeamTaskChangedPayload event payloads; re-export from the crate root.
- db: add ITeamRepository::list_messages_by_team (team-wide, created_at DESC,
  limit-capped) and list_messages_by_ids (batched IN lookup); implement for
  SQLite and all mock repos.
- team: add activity_mapping (row/domain -> response, files JSON degrades to
  empty on parse error), a team.mailboxChanged event constant, and
  TeamEventEmitter::broadcast_task_changed / broadcast_mailbox_changed.
- team: wire an optional emitter into Mailbox and TaskBoard via with_events;
  write/read-mark emit mailboxChanged, create/update emit taskChanged
  (deletion is a status=deleted update). Inject a single shared emitter at the
  sole production session construction site.
- team: add TeamSessionService::list_team_mailbox / list_team_tasks (ownership
  enforced, limit clamped to [1,1000], tasks sorted DESC with id tiebreak) and
  GET /api/teams/{id}/mailbox and /tasks routes.

Events log only non-sensitive correlation fields (id/change/team_id); message
content, summary, and task subject never appear in logs. Read endpoints log
team_id and result count at info. Adds unit and integration tests covering
ordering, limit clamping, mapping, emitter wiring, and ownership errors.
…29-001-001

# Conflicts:
#	crates/aionui-db/src/repository/sqlite_team.rs
#	crates/aionui-db/tests/team_repository.rs
#	crates/aionui-team/src/mailbox.rs
#	crates/aionui-team/src/session.rs
#	crates/aionui-team/src/task_board.rs
#	crates/aionui-team/src/test_utils.rs
#	crates/aionui-team/tests/common/mod.rs
#	crates/aionui-team/tests/session_service_integration.rs
… to user

These three team WebSocket events serialized their payload with a raw
serde_json::to_value, omitting the user_id that the event-bus → WebSocket
bridge requires. Since #669 (multi-account user scope isolation) the bridge
drops any non-whitelisted event lacking user_id, so these events never
reached the renderer: the activity board never updated task status, message
read/unread badges, or newly created tasks/messages in real time.

Route them through scoped_payload() like every other team event so the
bridge delivers them via broadcast_to_user, reusing the existing per-user /
per-team delivery scope. No bridge or global-whitelist changes needed.
The `SchedulerAction` enum, `TeammateManager::execute_action`, its private
`handle_*` arms, and `parse_tool_call` were a vestige of an earlier design
where MCP tool calls were parsed into `SchedulerAction`s and executed via
`finalize_turn(actions)`. The live path moved to `dispatch_tool` → `exec_*`
long ago: `parse_tool_call` was only ever called from unit tests, and
`execute_action` was only reachable from `finalize_turn`, which both
production callers invoke with an empty action slice. The crate's own
backend-audit already flagged `parse_tool_call` as dead code.

Remove the dead symbols and simplify `finalize_turn` to take just a slot id
(it now delegates straight to `mark_idle`, where the leader re-wake and
idle-notification bookkeeping already live). Tests that exercised only the
dead wrappers are dropped; those covering still-live behaviour
(`request_shutdown_agent`, leader re-wake on all-idle, idle-set-once) are
rewired to the live API so coverage is preserved.

No production behaviour change: the tool execution path (MCP + CLI, shared
via TeamToolExecutor → dispatch_tool) is untouched.
Assigning a team task to a teammate previously did nothing to notify or wake
the owner — the wake path was coupled only to mailbox messages (team_send_message
/ shutdown), so a lead that created a task without also sending a message left a
dormant owner asleep and the task unattended.

team_task_create / team_task_update now notify and lazily wake the assigned
teammate, and completing a task wakes the owners of any downstream tasks it
unblocks. Delivery reuses the exact agent-to-agent message path
(mailbox write + enqueue + event-loop notify), so an already-working owner has
the notice queued and drained on its next turn rather than interrupted, and a
dormant owner is warmed up — identical reliability to a direct message.

A pure gate (should_wake_task_owner) restricts wakes to a live, non-lead
teammate other than the caller, on a non-terminal, unblocked task; blocked
tasks wake later via the upstream-completion unblock path. The assignment notice
carries the task subject + description so the teammate (prompted to read its
mailbox for assignments) has the full hand-off without a separate message.

check_unblocks now also broadcasts team.taskChanged for each downstream task
whose dependency set changed, so the activity board reflects the unblock.

Lead prompt updated (Method Y): team_task_create auto-notifies/wakes the owner,
so the lead no longer needs a separate team_send_message to hand off or wake —
messages are for follow-up conversation.
Pre-existing formatting drift across a few crates plus one
clippy::cloned_ref_to_slice_refs lint in a mailbox test were failing the
`just push` gate (cargo fmt --all --check + clippy --workspace --all-targets
-D warnings). Format the affected files and replace `&[m.id.clone()]` with
`std::slice::from_ref(&m.id)`. No behaviour change.
Sync AionCore worktree baseline up to release v0.1.56.
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