# Async-Inform Rewind + Asymmetric Delivery + `full_steps` Restoration

**Date:** 2026-04-27
**Origin:** This document was written from a forked-off branch of a longer conversation. The main session does not have that fork's context. This file IS the briefing — read it in full before touching code.

**Status:** Awaiting implementation against the current uncommitted Meeting-008 amendment.

---

## ⚠️ READ THIS BEFORE ANYTHING ELSE — use `git diff` first

The Meeting-008 amendment landed in code but was **NEVER COMMITTED**. Before you modify a single line, run:

```bash
git diff -- core/async-inform.js tools/agent_message.js tools/agent_control.js infrastructure/database.js test/phase-3-suite.js docs/guide/06-tools.md docs/guide/09-multi-agent.md .meetings/meeting-009/
```

Use that diff to identify EXACTLY what was added by the Meeting-008 amendment vs what is legitimate Phase-3 code from prior meetings. Without this discipline you risk:

- **Accidentally deleting unrelated Phase-3 code** — `agent_control` tool, the trace endpoint, XML format conversion, the spawn-depth fix, the budget-resolver, etc. None of those should change.
- **Editing other things back to a "wrong" pre-Phase-3 shape** and breaking Phase-3 features that already shipped (tracking trace_id, instance_name, parentSessionId, etc.).
- **Reintroducing the duplicate-delivery bug** that the Meeting-008 fix was originally supposed to address. The fix went too far; the bug it was solving is still real and we still need protection against it (see §3).

The Meeting-008 amendment introduced these specific things that need to go away:

- A `while (iterations < MAX_DISPATCH_ITERATIONS)` outer dispatcher loop in `core/async-inform.js`.
- The `MAX_DISPATCH_ITERATIONS = 10` constant.
- The `_countPending` helper.
- `combinedContent` / `totalIn` / `totalOut` accumulation across multiple runChats.
- Drain-only iterations (`continueFlag: !initialMessage`).
- "EXACTLY ONE delivery per dispatch chain" comment + behavior — this mashed multiple runChats' replies into one delivery to A, which is wrong.
- The 70 KB animated HTML at `.meetings/meeting-009/max_dispatch_explanation.html` (the file explains a mechanism that should never have existed).
- Slide 5 observation + Slide 7 Card 3 + the carry-forward bullet about `MAX_DISPATCH_ITERATIONS` in `.meetings/meeting-009/meeting.md`.

Anything ELSE you see in the diff is legitimate Phase-3 work and stays.

---

## 1. What the user originally wanted (the bug Meeting-008 was solving)

The original Meeting-008 result Slide 2 comment from the user was:

> "When multiple agent_message messages sent to the same agent with `async_inform: true` they will both (or if there are more messages) will fire back the same result two times (or if there are more messages) so this edge case should be handled"

The bug this was pointing at: when ONE `runChat` on B drained N queued `agent_messages` rows, the system was satisfying N correlation_ids with the SAME response — meaning A could receive the same response delivered N times.

The correct fix is small: **collapse to ONE delivery per `runChat`, regardless of how many correlation_ids that runChat satisfied.** That is all.

What was implemented instead: the multi-runChat drain-everything-and-combine loop with a 10-iteration cap. Way too far.

---

## 2. The user's mental model (canonical — match this)

These are the user's two examples, verbatim:

### Example 1 — multiple messages while B is mid-loop

```
Agent A send msg#1
Agent B enter loop#1 with msg#1
Agent A send msg#2 (while Agent B still in loop#1)
Agent B in loop#1 gets msg#2 injected between tool calls
Agent A send msg#3 (while Agent B still in loop#1)
Agent B in loop#1 gets msg#3 injected between tool calls
Agent B finishes
The final response of agent B gets send back to the Agent A 3 times (my fucking fix here, send it back only once)
```

### Example 2 — sequential messages

```
Agent A send msg#1
Agent B enter loop#1 with msg#1
Agent B finishes
The final response of agent B gets send back to the Agent A
Agent A send msg#2
Agent B in loop#2 gets msg#2 injected between tool calls
Agent B finishes
The final response of agent B gets send back to the Agent A (normal behaviour)
```

Layer 1 (in-flight injection at iteration boundaries via `drainNonFollowup`) IS THE QUEUE SYSTEM. It already works. Don't rebuild it. Don't replace it. It's correct.

---

## 3. The corrected NEW behavior model — asymmetric msg#1 vs msg#2+

After the fork's discussion, the user clarified an asymmetric design that supersedes both the Meeting-008 amendment AND the naive "one delivery per runChat" reading of Examples 1/2 above. The asymmetry:

### Roles

- **`msg#1`** = the message that started B's runChat. Its reply is what A "expects" from the long-running task.
- **`msg#2+`** = any subsequent message A sends while B is still in that runChat. Should get a quick mid-run acknowledgment so A doesn't block waiting forever for the full task to finish.

### Delivery rules

- **`msg#1` reply** = the **final** text content of B's runChat (the last LLM call's text, the one with no tool_calls). Delivered when the runChat ends.

- **`msg#2+` reply** = the **first text content B produces in the LLM call that runs immediately after `drainNonFollowup` picked up that message**. Delivered IMMEDIATELY mid-runChat — A does not wait for the loop to finish.

So in Example 1, the corrected behavior is:

```
A sends msg#1 → B starts loop#1
A sends msg#2 (mid-loop)
  → at next iteration boundary, msg#2 drained
  → next LLM call produces text T2
  → T2 fired to A immediately as msg#2's reply
A sends msg#3 (still mid-loop)
  → at next iteration boundary, msg#3 drained
  → next LLM call produces text T3
  → T3 fired to A immediately as msg#3's reply
… B continues looping …
B finishes loop#1 with final text Tfinal
  → Tfinal fired to A as msg#1's reply
```

Three deliveries to A. msg#1 → final text. msg#2 → first intermediate text after its drain. msg#3 → first intermediate text after its drain.

### Edge case — msg#2+ arrives in B's last iteration

If `msg#2+` arrives so late that the LLM call right after its drain IS the final iteration of B's runChat (no more tool_calls returned, runChat about to finalize):

- There is no "intermediate" text to use as msg#2+'s reply — the next text is the FINAL text.
- **Resolution:** deliver the final text as `msg#1`'s reply (normal). **IGNORE the msg#2+** — drop its delivery entirely. Reasoning: that delivery would be byte-identical to msg#1's, and a duplicate is meaningless. Mark the queued row as delivered with `(no separate reply — coincided with msg#1's final)` or similar audit text so the queue isn't left with status='pending'.

---

## 4. Three additional behavioral rules

### 4.1 Sender field includes session ID

When an agent sends a message to another, the `from_agent` field on the `agent_messages` row should carry **`<agent_name>:<session_id>`** (or an equivalent structure if the schema supports it). Apply to both directions: the original A→B enqueue, AND the reply that gets delivered back to A.

Specifically, the wrapping currently in `dispatchAsyncInformTurn` like:

```js
const text = `[Message from ${instance_name || agent}]: ${result.content || '(no response)'}`;
```

…should include the session ID as well, so A's session can disambiguate which subagent session the reply is from when there are multiple instances.

Verify the existing `agent_messages` schema has a `from_session_id` column (it does, from Phase 2). Use it. The wrapping text should also surface the session ID for the LLM's benefit:

```js
const text = `[Message from ${instance_name || agent} (session: ${targetSessionId})]: …`;
```

### 4.2 Tool description note for `agent_message`

Explicitly document in the tool's `description` and the `async_inform` parameter description that:

- Sending a SECOND async message to an agent that is already running returns the **first mid-run response** (NOT the last/final response).
- `full_steps` has no effect on mid-run replies (per rule 4.3).

This goes in the JSON schema's `description` strings so the LLM agents see it.

### 4.3 `full_steps` is meaningful only for msg#1 (final-delivery) replies

For `msg#2+` (mid-run replies), `full_steps` is **silently ignored** — the reply is whatever text content the LLM produced in that one iteration, no full-steps formatting.

`full_steps` only takes effect when applied to the message that becomes the msg#1 of a runChat (i.e., the one whose reply is the final text).

---

## 5. Restoring `full_steps` parameter

`full_steps` was removed during the Phase-3 `agent_message` redesign. Bring it back.

### 5.1 Schema addition

Add `full_steps: boolean` to `agent_message`'s `input_schema.properties` with a description that includes the rule from §4.3.

### 5.2 Persistence

Add a `full_steps` column to the `agent_messages` table via `ensureColumn` in `infrastructure/database.js` so the flag travels with the queued row through the dispatcher.

```js
ensureColumn(db, 'agent_messages', 'full_steps', 'INTEGER NOT NULL DEFAULT 0');
```

`enqueueAgentMessage` should accept `fullSteps` and write it.

### 5.3 Format spec

When `full_steps: true` AND the message becomes msg#1 of its runChat (i.e., the dispatcher is about to deliver the final text), the delivery contains all assistant messages and tool calls B produced after receiving the user message, formatted line-per-line:

```
<assistant text content>
>>>> TOOL_CALL name="<tool>" <input-json truncated to 100 chars> <<<<
<assistant text content>
>>>> TOOL_CALL name="<tool>" <input-json truncated to 100 chars> <<<<
```

Tool-call payload truncated to 100 chars. Source rows: `messages` table where `session_id = B_sid` AND `created_at > <user-message-timestamp>` AND `role IN ('assistant', 'tool_use')` (exact role names per existing schema).

### 5.4 Multi-message accumulation rule

If multiple `async_inform` messages got accumulated into one runChat (Example 1 territory) AND **the msg#1 of that runChat** had `full_steps = true`, the final delivery uses full-steps format. msg#2+ deliveries are unaffected (regular intermediate text only) per §4.3.

---

## 6. Files affected

| File | What to do |
|---|---|
| `core/async-inform.js` | Gut the Meeting-008 loop (cap, _countPending, combinedContent, drain-only iterations). Rewire to: ONE runChat call, hook for mid-runChat deliveries (msg#2+), final delivery for msg#1, edge-case handling. |
| `tools/agent_message.js` | Re-add `full_steps` to schema. Update tool description per §4.2. Strip Meeting-008 references in comments. Async branch's in-flight detection logic stays (one dispatch per (A,B) at a time). |
| `infrastructure/database.js` | Add `full_steps` column to `agent_messages` via `ensureColumn`. Update `enqueueAgentMessage` to accept and write `fullSteps`. |
| `core/loop.js` and/or `engines/claude-engine.js` | Add the mid-run delivery hook: after the LLM call that follows a `drainNonFollowup` pickup of a non-msg#1 queued message, fire a delivery to A immediately. Needs cooperation with the dispatcher to know which queued items are msg#1 vs msg#2+ for THIS runChat. |
| `test/phase-3-suite.js` | Drop M008-3 assertions (about `MAX_DISPATCH_ITERATIONS`, drained, "EXACTLY ONE delivery"). Add: Example-1 scenario (3 deliveries, asymmetric), Example-2 scenario (2 deliveries sequential), edge-case test (msg#2 at last iteration → ignored), `full_steps` format test, multi-message full_steps rule test, sender-includes-session-id test. |
| `docs/guide/06-tools.md` | Strip drained/combined/EXACTLY-ONE language. Document `full_steps`. Document the asymmetric msg#1 vs msg#2+ behavior. |
| `docs/guide/09-multi-agent.md` | Same strip + document. |
| `.meetings/meeting-009/max_dispatch_explanation.html` | DELETE. |
| `.meetings/meeting-009/meeting.md` | Remove Slide 5 observation about `MAX_DISPATCH_ITERATIONS`, Slide 7 Card 3 assumption ("dispatch cap of 10 is enough"), and the carry-forward bullet about tuning it. |

---

## 7. Sequencing instructions for the implementer

1. **Run `git diff`** on the files in §0 to see the Meeting-008 additions in context.
2. **Identify pre-Meeting-008 dispatcher logic** in `core/async-inform.js`. Look for the original simple dispatcher that just fires one runChat and delivers the result. That logic is your starting point.
3. **Strip the Meeting-008 over-engineering** (the loop, the cap, the combined-content, the drain-only iterations).
4. **Layer the asymmetric model** on top of the simple dispatcher:
   - Track which queued rows are msg#1 (the runChat's seed) vs msg#2+ (drained mid-loop).
   - Wire mid-runChat delivery firing for msg#2+.
   - Wire final-runChat delivery for msg#1.
   - Implement edge-case (msg#2+ at last iteration → ignore).
5. **Re-add `full_steps`** per §5.
6. **Strip docs** per §4.2 + §6.
7. **Update tests** per §6.
8. **Delete the animation file** per §6.
9. **Update Meeting-009 deck** per §6.
10. **Run `node test/phase-3-suite.js`** and confirm fully green before declaring done.
11. **Do NOT commit** unless the user explicitly asks. The user has been holding the diff intentionally.

---

## 8. Acceptance criteria

After your changes:

- `git grep MAX_DISPATCH_ITERATIONS` returns zero hits in active code (only in commit history if the user later commits this rewind).
- `git grep _countPending` returns zero hits.
- `git grep "EXACTLY ONE"` returns zero hits in code/docs.
- `git grep combinedContent` returns zero hits.
- `git grep full_steps` returns hits in `tools/agent_message.js`, `infrastructure/database.js`, `docs/guide/06-tools.md`, and `test/phase-3-suite.js`.
- `node test/phase-3-suite.js` runs fully green.
- `.meetings/meeting-009/max_dispatch_explanation.html` does not exist.
- `.meetings/meeting-009/meeting.md` has no mention of `MAX_DISPATCH_ITERATIONS`.
- Tool descriptions for `agent_message` carry the §4.2 note about second-async returning first mid-run response.
- Cross-agent message wrapping shows `<agent_name> (session: <session_id>)`.

---

## 9. What this document is NOT

- It is NOT a request to redesign Phase 3. The Meeting-007 plan, Meeting-008 implementation, Phase-3 integration suite, agent_control tool, trace endpoint, XML format, budget governor, spawn-depth check — all of that stays. Only the Meeting-008 amendment to `core/async-inform.js` (and its test/doc/animation echoes) is being reverted and replaced.
- It is NOT a critique of Phase 3 broadly. Phase 3 shipped clean. This is one specific over-engineered patch being walked back.
- It is NOT the place to introduce new features beyond §4 (sender-with-session-id) and §5 (full_steps restoration). Anything else is out of scope.
