iOfficeAI / iOfficeAI/AionCore

[Bug] Team mode's wake payload wrapping breaks ACP slash-command detection for direct human→teammate messages (e.g. /compact, /init)

Open Beginner friendly
#687 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
105
Forks
169
Avg merge
5h 58m
Merged PRs (30d)
84

Description

## Summary

When a user sends a direct message to a teammate in Team mode (`POST /api/teams/{id}/agents/{slot_id}/messages`), the raw text is never delivered to the underlying ACP agent as-is. It gets wrapped inside a "wake payload" (unread messages + task board summary + role reminder) before being forwarded as the turn's prompt content. Any ACP agent whose slash-command detection requires the command to be the literal first characters of the prompt (e.g. opencode's `detectSlashCommand`, which checks `text.trim().starts_with("/")`) can never recognize a slash command sent this way — it silently falls through and gets treated as plain conversational text.

This affects **all** slash commands for direct human→teammate sends in Team mode, not a specific command. Confirmed with `/init` (opencode built-in, guaranteed to be registered) and a custom-registered command — both fail identically in Team mode while working correctly in a standalone (non-team) conversation with the same agent/backend.

## Environment

- AionCore: built from `main` (code paths below present as of this writing)
- Affected backend: opencode (ACP), reproduced against `opencode acp`; likely affects any ACP agent whose slash-command parsing requires the prompt to start with `/`
- Affected path: Team mode direct message to a teammate (`sendMessageToAgent` / `POST /api/teams/{id}/agents/{slot_id}/messages`), NOT the Leader's own turn, NOT a standalone conversation

## Steps to Reproduce

1. Create a team with at least one opencode-backed teammate.
2. Open a **standalone** (non-team) conversation with the same opencode agent/backend, send `/init` — confirm it correctly triggers the built-in guided `AGENTS.md` setup flow.
3. In the **Team** view, send `/init` directly to the teammate (same underlying opencode backend).
4. Observe: instead of triggering the guided setup, the text is treated as a normal user prompt and the model responds conversationally, exactly as if the leading `/` were not there.

## Expected

A direct message to a teammate that opencode/ACP would recognize as a slash command in a standalone conversation should be recognized the same way in Team mode.

## Actual

The command is silently swallowed into conversational text. No error, no indication that command parsing was skipped.

## Root Cause Analysis

Traced the full path from HTTP handler to the actual prompt content delivered to the agent runtime:

1. `crates/aionui-team/src/routes.rs` → `send_message_to_agent` → `crates/aionui-team/src/service.rs::send_message_to_agent` → `crates/aionui-team/src/session.rs::send_message_to_agent` → `enqueue_user_message`.
2. `enqueue_user_message` writes the **raw, unmodified** text into the mailbox (`crates/aionui-team/src/mailbox.rs::write_with_files` — `content: content.to_owned()`, no transformation here).
3. When the teammate's event loop next claims a batch of unread mailbox messages (`crates/aionui-team/src/session.rs`, around line 358), it builds the actual turn content via `build_wake_payload` (`crates/aionui-team/src/prompts/mod.rs`):

```rust
pub fn build_wake_payload(...) -> String {
let mut payload = String::with_capacity(2048);
payload.push_str("## New Messages\n\n");
for msg in unread_messages {
payload.push_str(&format!(
"- From `{}` [{}]: {}\n",
msg.from_agent_id, type_label, msg.content, // <-- the user's raw text lands here, mid-string
));
}
payload.push_str(&wake_summary::render_task_board_summary(agent, tasks, current_slot_ids));
payload.push_str(&format!(
"You are **{}** (role: {}). Proceed with your work.\n",
agent.name, agent.role,
));
payload
}
```

4. `session.rs` (~line 358): `let wake_body = build_wake_payload(...)`, optionally prefixed with the role prompt (`format!("{role_prompt}\n\n{wake_body}")`), assigned to `first_message`.
5. `crates/aionui-team/src/event_loop.rs:284`: `content: input.first_message` — this is what actually gets sent to the agent runtime as the turn's prompt.

So a user-typed `/init` or `/compact` ends up delivered as something like:

```
## New Messages

- From `user` [message]: /init

## Current Task Board Summary
...

You are **Worker1** (role: teammate). Proceed with your work.
```

opencode's ACP layer (`packages/opencode/src/acp/service.ts`, `detectSlashCommand`) requires `text.trim().starts_with("/")` on the **entire** joined text content of the prompt. Since the actual first characters are `"## New Messages"`, not `"/"`, the command can never be detected — regardless of whether the command itself is registered/known to the agent.

## Evidence strength (being honest)

- "Direct-to-teammate sends go through `build_wake_payload`, which prepends non-command text before the user's message" — **confirmed from source**, full call chain traced above.
- "Standalone (non-team) conversations do not go through this wrapping" — **confirmed by reproduction**: the identical `/init` command works correctly outside Team mode against the same agent/backend.
- "opencode's `detectSlashCommand` requires the command to be the literal start of the text" — **confirmed from opencode source** (`packages/opencode/src/acp/service.ts`).
- "This affects all ACP backends, not just opencode" — **inference, not verified**: any ACP agent that gates command parsing on the prompt's leading characters would have the same problem; only opencode was reproduced here.

## Suggested Fix

Minimal, targeted change in `build_wake_payload` (`crates/aionui-team/src/prompts/mod.rs`): when a wake is triggered by exactly one unread, human-authored `Message`-type mailbox entry that looks like a slash command, deliver it verbatim instead of wrapping it. This preserves the existing wrapping behavior for every other case (multiple messages, idle notifications, shutdown requests, non-command text):

```rust
pub fn build_wake_payload(
agent: &TeamAgent,
tasks: &[TeamTask],
unread_messages: &[MailboxMessage],
current_slot_ids: &HashSet,
) -> String {
// A single, direct human message that looks like a slash command is
// delivered verbatim so the underlying ACP agent's own slash-command
// detection (which requires the command to be the very first characters
// of the prompt) can recognize it. Wrapping it in the usual
// "## New Messages" / task-board / role-reminder scaffolding silently
// makes every slash command unusable in Team mode.
if let [only] = unread_messages {
if only.msg_type == MailboxMessageType::Message && only.content.trim_start().starts_with('/') {
return only.content.clone();
}
}

let mut payload = String::with_capacity(2048);
// ...unchanged
}
```

A more thorough fix might also special-case this earlier (e.g. in `enqueue_user_message`) so it's explicit that this is a "direct command passthrough" rather than relying on wake-payload shape, but the above is the minimal, low-risk change consistent with the existing code structure.

## Related

- `iOfficeAI/AionCore#679` — a related but distinct bug: standalone (non-team) Codex slash commands were unrecognized because the ACP bridge stopped advertising them (`available_commands` was empty), not because of message wrapping. Different root cause, same symptom class (slash command silently treated as plain text).

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in crates/aionui-team/src/prompts/mod.rs at build_wake_payload, then trace its callers in session.rs and the prompt handoff in event_loop.rs:284. Reproduce the Team-mode direct-message case with /init or /compact and compare it with a standalone conversation. Done means a qualifying direct slash command reaches the ACP agent as the prompt's leading text while other wake payloads remain wrapped.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
api, backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.