# 0.38.6 — `k2so msg`: deliver-or-fail, with sender identity

**Author:** Rosson
**Date:** 2026-05-21
**Status:** Approved, ready for implementation
**Scope:** Daemon-side + CLI wrapper + generated-template rewrite.
Tauri stays a thin client (no renderer changes).

## TL;DR

`k2so msg` is the fundamental inter-agent-communication primitive. Today
it's chronically unreliable: every C3PO ticket about it tells the same
story — the first call returns success but doesn't land; the second
identical call lands. Multiple delivery code paths, two JSON response
shapes, a misleading `injected_to_pty: true` field, and a silent inbox
fallback have made the tool unusable without re-fire superstition.

0.38.6 makes `msg` strictly **deliver-or-loudly-fail**, with one
canonical response shape, an auto-derived `--from`, an internal retry
window for the transient race that drives the re-fire pattern, and a
rewrite of the generated templates that still recommend deprecated
forms.

## Problem evidence

From C3PO project K2SO (`034f029a-...`):

- `d3ca5e77` — first `--wake` returned legacy shape
  `{injected_to_pty: true, published_to_bus: true}`, didn't land. Second
  attempt returned new shape `{branch, delivery: "live", targetSessionId}`,
  landed. **Two paths, two shapes, no way for sender to tell which fired.**
- `d3dc9b7d` — manager onboarding a peer agent (Julie) saw repeated
  silent `delivery: inbox` returns despite a live Claude session in the
  target. **Silent inbox fallback masks the real reason for failure.**
  Diagnosis amendment (2026-05-21 19:58 UTC) traced this to
  `workspace_sessions.active_terminal_id` being unbound at first call.
- `4f4d9c1d` — generated CLAUDE.md / AGENT.md templates still recommend
  the deprecated `--agent <name>` and `<workspace>:inbox` forms. Every
  onboarding agent reads stale instructions.
- `e76d2ff9` — `k2so msg --help` errors instead of showing help (agents
  try `--help` first by reflex).

## Goals

1. **`msg` becomes live-or-fail.** Success means "the bytes have landed
   in the recipient's PTY and the PTY's child is alive." Failure means
   the message did NOT deliver, with a specific `reason` + actionable
   `hint`. No silent inbox fallback.
2. **One JSON response shape** every call, every branch.
3. **Auto-derived `--from`** with hard-fail when neither CWD/env nor
   explicit `--from` resolves. Every delivered message carries a real
   sender identity.
4. **Internal retry window** absorbs the transient race that drives the
   chronic re-fire pattern. Caller still makes one CLI call; the daemon
   internally retries before giving up.
5. **Workspace name is the lead form** in docs. UUID + path stay
   supported but live below the fold.
6. **Generated templates rewritten** so onboarding agents read the
   correct contract.

## Non-goals (deferred to 0.38.7)

- Single canonical-session writer (consolidating `ensure_canonical_session`,
  dropdown, renderer-open path).
- Pinned-tab bootstrap so `active_terminal_id` is populated at
  workspace-creation time.
- First-turn session-id detection for `claude --resume` continuity.
- Renaming the dual "pinned" concepts (sidebar pin vs active terminal).
  Documentation only.

## The model

```
k2so msg <workspace> "text" [--from <name>]
```

**One verb, one outcome: live delivery or loud failure.**

- For queued tasks (recipient reads on their schedule): `k2so work send`.
- For raw PTY injection into any session: `k2so terminal write <id>`
  (documented in `k2so help --advanced`).

## Canonical response shape

Every `msg` call returns exactly this shape:

```json
// Success
{
  "success": true,
  "target_session_id": "<uuid>",
  "attempts": 1
}

// Failure
{
  "success": false,
  "target_session_id": null,
  "attempts": 2,
  "reason": "no_agent_mode",
  "hint": "Workspace has no agent. Use `k2so work send` to queue, or `k2so mode custom` to set up an agent."
}
```

Fields:

| Field | Type | Meaning |
|---|---|---|
| `success` | bool | True iff bytes landed in a live PTY whose child is alive. |
| `target_session_id` | uuid \| null | The PTY/session the bytes landed in. Null on failure. |
| `attempts` | int | Number of delivery attempts made (1 = first try succeeded; 2+ = retry hit). |
| `reason` | string \| null | Failure code (see table below). Null on success. |
| `hint` | string \| null | Human-readable next-step suggestion. Null on success. |

`target_session_id` doubles as the escape-hatch handle: on failure,
operators can use `k2so terminal write <target_session_id>` to inject
manually.

CLI exit code: `0` on success, `2` on any failure reason.

## Failure reasons + hints

| `reason` | When it fires | `hint` |
|---|---|---|
| `workspace_not_found` | `<workspace>` arg didn't resolve | "Run `k2so connections list` to see available workspaces." |
| `no_agent_mode` | Workspace exists, AGENT.md absent / mode off | "Workspace has no agent. Use `k2so work send` to queue, or `k2so mode custom` to set up an agent." |
| `spawn_failed` | Cascade tried fresh-fire, claude binary failed | "Spawn failed: \<stderr excerpt\>. Verify `claude` is on PATH for the daemon." |
| `pty_died` | PTY existed but child exited before write completed | "Target session crashed during delivery. Check `~/.k2so/daemon.stderr.log`." |
| `no_sender_identity` | Sender CWD + `--from` both unresolvable | "Pass `--from <name>` explicitly, or run from inside a registered workspace." |

## `--from` semantics

**Resolution order:**

1. Explicit `--from <name>` flag wins.
2. Sender's workspace name, resolved from CWD or `K2SO_PROJECT_PATH`.
3. **Hard fail** with `reason: "no_sender_identity"`. Never default to
   "unknown" / empty / "external".

**Rendered format in recipient's PTY:**

```
[from scout_v3] hello world
```

Single-line compact prefix. Easy to grep, easy for claude to parse,
no terminal-width concerns. Implementation: prepend to the bytes at
the same point as `inject_bytes_for_signal` formats signals today
(`crates/k2so-daemon/src/signal_format.rs` or equivalent — TBD by
implementer based on where `deliver_live` writes).

**Multi-line text** keeps the prefix on the first line:

```
[from scout_v3] Here's the briefing:

Section 1: ...
Section 2: ...
```

(claude's first-line parse picks up the sender; subsequent lines flow
naturally.)

## Internal retry window

The empirical observation across every report: **re-fire almost always
succeeds.** Root cause is timing — `active_terminal_id` becomes populated
as a side-effect of the first call's fresh-fire spawn, so the second
call hits Branch 1 instead of Branch 3. After 0.38.6 collapses to one
delivery path, that race shrinks but doesn't disappear (PTY-readiness
races after fresh spawn still exist).

**Retry policy, daemon-side, hidden from the caller:**

```
deliver(workspace, text, from) -> Response:
    for attempt in 1..=MAX_ATTEMPTS:
        result = attempt_delivery(workspace, text, from)
        if result.success:
            return result with attempts=attempt
        if result.reason in PERMANENT_REASONS:
            return result with attempts=attempt
        sleep(BACKOFF[attempt - 1])
    return final_result with attempts=MAX_ATTEMPTS
```

- `MAX_ATTEMPTS = 3` (1 initial + 2 retries).
- `BACKOFF = [200ms, 400ms]` between attempts. Worst-case total: ~600ms.
- `PERMANENT_REASONS = {workspace_not_found, no_agent_mode, no_sender_identity}`
  — these won't fix themselves by waiting. Surface immediately.
- `TRANSIENT_REASONS = {spawn_failed, pty_died}` + the implicit
  "write returned EAGAIN / EPIPE" path — retry.

The `attempts` field in the response surfaces whether retry was needed,
which doubles as debug telemetry without polluting the success contract.

## CLI surface — `cli/k2so`

### `cmd_msg()` (around line 1856)

Rewrite to:

1. Strip `--wake` from argv parsing. Accept it for one release as a
   silent no-op (warn to stderr but don't fail) so external scripts don't
   break overnight. Drop entirely in 0.39.x.
2. Parse `--from <name>` flag. Pass through to daemon.
3. Auto-derive `--from` from `K2SO_PROJECT_PATH` env or `pwd` if flag
   absent. Pass empty string to daemon if neither resolves; daemon will
   return `no_sender_identity`.
4. Handle `--help` / `-h` before "message text required" validation
   (closes `e76d2ff9`).
5. Route every call through `workspace_msg::deliver_live` — no
   conditional branch on `--wake`.
6. Exit `0` on `success: true`, exit `2` on any failure.

### Help text (around line 3017)

```
msg <workspace> "text" [--from <name>]
    Deliver a message to a workspace's agent, live.
    Spawns the agent if not already running. Returns success only
    after the message has landed in the agent's session.

    <workspace>    Workspace name (see `k2so connections list`).
                   Absolute path or UUID also accepted.
    --from <name>  Sender identity. Defaults to sender's workspace.

    For queued tasks (recipient reads on their schedule):
      k2so work send
    For raw PTY injection into any session:
      k2so help --advanced  (see TERMINAL section)
```

### `k2so help --advanced`

Add a paragraph under MESSAGING explaining the `terminal write <id>`
escape hatch, and another under TROUBLESHOOTING explaining the
`target_session_id` in `msg` responses can be used as the `<id>` arg
to `terminal write` for manual recovery.

## Daemon — `crates/k2so-daemon/src/`

### `workspace_msg.rs` — the canonical delivery function

Already exists with the right cascade structure. Changes:

1. **Add `from: &str` parameter** to `deliver_live`. Thread through
   `inject_live`, `resume_and_fire`, `fresh_fire`. Pass to the bytes
   formatter so every delivery gets the `[from <name>] ` prefix.
2. **Wrap the cascade in the retry loop** described above. The current
   `deliver_live` becomes `attempt_delivery`; the new `deliver_live` is
   the retry-aware wrapper.
3. **Normalize the response shape.** Every return path from
   `attempt_delivery` returns the canonical struct (define in this file:
   `pub struct MsgResponse { success, target_session_id, reason, hint, ... }`)
   serialized via `serde_json::to_value`. Strip old fields:
   `branch`, `delivery`, `targetSessionId` (camelCase), `error`, etc.
4. **Liveness check before claiming success.** After `live.write(...)`
   returns Ok, confirm `live.child_alive()` is still true (or equivalent
   via `DaemonPtySession`). If the child exited during the write, return
   `pty_died` instead of success — this is one of the corner cases that
   today produces false-positive `injected_to_pty: true`.
5. **Workspace resolution at the top.** Accept workspace name or path
   or UUID; convert to `project_id` early. On failure, return
   `workspace_not_found` before touching `WorkspaceSession`.

### `awareness_ws.rs` — retire the legacy path

`awareness_ws.rs` contains the legacy `injected_to_pty` / `published_to_bus`
response shape that the first failed call from `d3ca5e77` returned. Audit
its callers:

- If the path is reachable only via deprecated CLI forms
  (`msg --agent <name>`, the `:inbox` suffix), retire by replacing
  invocations with calls to `workspace_msg::deliver_live`.
- If any internal caller still depends on the old shape (heartbeat?
  awareness bus?), preserve the function but rename to make its scope
  obvious (e.g., `legacy_pty_inject_for_awareness_bus`) and add a deny
  list / debug-assert that no `--wake` path reaches it.

Goal: after 0.38.6, the only way to get the legacy shape is through an
explicit legacy code path that `msg` no longer exercises. By 0.39.x
this function can be deleted.

### `signal_format.rs` (or wherever bytes are formatted)

Add the `[from <name>] ` prefix render. Single function, used by every
`inject_live` / `resume_and_fire` / `fresh_fire` path so the prefix is
applied consistently.

## Generated templates — `crates/k2so-core/src/agents/skill_content.rs`

Lines 353, 354, 412, 413, 540, 541 contain the deprecated examples:

```
k2so msg <workspace>:inbox "description of work needed"
k2so msg --wake <workspace>:inbox "urgent — wake the agent"
```

Rewrite to:

```
k2so msg <workspace> "text"                          # deliver to the running agent
k2so work send <workspace> --title "..." --body "..."  # queue a task
```

Drop the `:inbox` suffix entirely (it's vestigial per `4f4d9c1d`).
Drop the `--wake` flag (no longer needed).

Audit `delegate.rs:225,236` and `build_launch.rs:87` for similar stale
`--agent <name>` instructions in agent CLAUDE.md content. Update where
relevant to the workspace-name-led form.

## Testing strategy

### Unit tests (workspace_msg.rs)

1. **Success cases** — one test per branch (`active_terminal_id`,
   `argv_scan`, `resume_session`, `fresh_spawn`). Each asserts the
   canonical response shape with `success: true`, populated
   `target_session_id`, `attempts: 1`.
2. **Failure cases** — one test per `reason`. Each asserts `success:
   false`, the expected `reason`, a non-empty `hint`.
3. **Retry behavior** — mock a transient failure on attempt 1
   (return `pty_died`), success on attempt 2. Assert final response is
   success with `attempts: 2`.
4. **Permanent-reason short-circuit** — mock `no_agent_mode` on
   attempt 1. Assert no retry (attempts: 1).
5. **`--from` prefix rendering** — assert the bytes written to the PTY
   start with `[from <name>] `.
6. **`no_sender_identity` failure** — pass empty `--from`, assert hard
   fail.

### Integration smoke tests (existing harness)

1. Fresh workspace, mode flip, `msg <workspace> "hi"` → success on
   first call (retry absorbs the active-terminal race).
2. Re-fire identical message → success with `attempts: 1`.
3. `msg nonexistent-workspace "hi"` → `workspace_not_found`.
4. `msg <mode-off-workspace> "hi"` → `no_agent_mode`.
5. Bytes in the recipient's PTY start with `[from <sender-name>] `.

### Acceptance criteria

- Every test in the existing `cargo test --workspace` suite passes.
- New test count: existing total + ~10 (the cases above).
- C3PO repro from `d3ca5e77` no longer triggers (first call returns
  success that actually delivered).
- C3PO repro from `d3dc9b7d` no longer triggers (no silent inbox
  fallback when target has a live session).
- C3PO repro from `e76d2ff9` no longer triggers (`--help` works).

## File-by-file change plan

| File | Change |
|---|---|
| `crates/k2so-daemon/src/workspace_msg.rs` | Add `from` param + retry loop + canonical `MsgResponse` struct + liveness check + workspace name resolution. |
| `crates/k2so-daemon/src/awareness_ws.rs` | Audit legacy `injected_to_pty` path; ensure no `msg` call reaches it. |
| `crates/k2so-daemon/src/signal_format.rs` (or wherever `inject_bytes` lives) | Add `[from <name>] ` prefix formatter. |
| `cli/k2so` (~line 1856) | Rewrite `cmd_msg()`: drop `--wake` parsing, add `--from`, auto-derive sender, route everything through `workspace_msg::deliver_live`, fix `--help` ordering, exit-code semantics. |
| `cli/k2so` (~line 3017) | Rewrite `msg` help text to the ~10-line version. |
| `cli/k2so` (`--advanced` section) | Add MESSAGING + TROUBLESHOOTING paragraphs about `terminal write` escape hatch. |
| `crates/k2so-core/src/agents/skill_content.rs` | Replace 6 lines of deprecated `:inbox` / `--wake` examples with the new workspace-name-led form. |
| `crates/k2so-core/src/agents/delegate.rs:225,236` | Audit + update if `--agent <name>` references leak into generated CLAUDE.md. |
| `release-notes-0.38.6.md` | Author. |

## Rollout

1. Implement daemon changes first; gate behind unit tests.
2. Implement CLI changes; gate behind shell integration tests.
3. Implement template rewrites; gate behind a check that no skill file
   contains `:inbox` or `--wake` examples.
4. Run full `cargo test --workspace` — must be green.
5. Smoke test against dev daemon: every reason code reproducible, every
   success path returns canonical shape.
6. Ship via `./scripts/release.sh 0.38.6 release-notes-0.38.6.md`.

## Open questions for implementer

1. **Where does `inject_bytes_for_signal` live in 0.38.5?** The PRD
   assumes a single bytes formatter; if there are several (heartbeat,
   awareness signal, msg) they all need the `[from <name>] ` prefix or
   the prefix needs to live in `deliver_live` instead.
2. **Should `target_session_id` reflect the post-retry session, or the
   first-attempt session?** Recommendation: post-retry — that's the
   session the bytes actually landed in. Callers should treat the field
   as "where your message went," not "where we first tried."
3. **Do we surface `attempts` in the success case at all, or only in
   the failure case?** Recommendation: always surface. It's a useful
   debug signal for telemetry/operators ("our msg success rate via
   retry is X%").

## Definition of done

- `k2so msg <workspace> "hello"` from a freshly-created agent workspace
  returns `success: true` on the first call, with the bytes landing in
  the recipient's PTY prefixed with the sender's workspace name.
- No `k2so msg` call ever silently writes to inbox.
- `k2so help msg` is ~10 lines and points at `work send` + `--advanced`.
- Generated CLAUDE.md / AGENT.md templates contain no `:inbox` /
  `--wake` / `--agent <name>` recommendations.
- `cargo test --workspace` green; release shipped.

