Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.

feat(kora): KR-EMAIL-OUTBOUND-REASONING-META — add reasoning meta to email outbound log#146

Merged
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-EMAIL-OUTBOUND-REASONING-META
May 23, 2026
Merged

feat(kora): KR-EMAIL-OUTBOUND-REASONING-META — add reasoning meta to email outbound log#146
rafe-walker merged 1 commit into
feature/phase2-upgradesfrom
feat/kora-KR-EMAIL-OUTBOUND-REASONING-META

Conversation

@rafe-walker

Copy link
Copy Markdown
Owner

Summary

Closes the gap CC#2 caught via STOP-ASK on KR-REASONING-PANEL-EMAIL-XREF: `email_outbound_log.jsonl` was missing the model_used / tokens / duration / reasoning_error / caller_session_id fields that slack_dm's outbound has had since PR #131. Mirrors that exact pattern for the email path.

Bucket spec: `17_cc_bucket_prompts/KR-EMAIL-OUTBOUND-REASONING-META_extension.md`

Reference PRs:

Surface (additive + backwards-compatible across 3 files)

Layer Net
`kora_cli/clients/purelymail_client.py` `send_email()` + `_append_outbound_log()` + `send_email_internal()` each gain 6 optional kwargs (`model_used`, `input_tokens`, `output_tokens`, `reasoning_duration_ms`, `reasoning_error`, `caller_session_id`). All default `None`. Opt-in JSONL inclusion (key omitted when `None`) — pre-bucket entry shape preserved for non-reasoning callers.
`kora_cli/handlers/email_inbound_handler.py` `_call_reasoning_engine` return type flipped from `(reply_text, reasoning_error)` tuple to `(reply_text, reasoning_meta_dict)`. New helpers: `_empty_reasoning_meta`, `_reasoning_meta_from_result`, `_email_caller_session_id`. New small helper `_build_reply_and_meta` consolidates the engine-None / engine-call dispatch. `_send_auto_reply` threads meta + caller_session_id into `client.send_email(**meta, caller_session_id=...)`.
Tests 12 new (5 client JSONL shape + 7 handler meta threading)

caller_session_id format — documented + enforced consistent

Per CC#2's spec proposal: `f"email:{message_id}"`. This matches the reasoning engine's own derivation at `kora_cli/reasoning/anthropic_engine.py:869-871` (in `_derive_caller_session_id` for `source="email"`). Both sides on the same literal string lets KR-REASONING-PANEL-EMAIL-XREF join audit ↔ outbound JSONL rows by a single field.

Test `test_caller_session_id_matches_engine_derivation_shape` proves this: it imports the engine's private helper, constructs an `IncomingMessage` exactly the way the handler does, and asserts the handler's derived `caller_session_id` equals the engine's output for the same `ParsedIncomingEmail`.

§1 K-DG verification at HEAD `b959737b`

All 3 line-refs from the bucket spec still accurate:

  • `purelymail_client.py:521` `_append_outbound_log()` ✓
  • `email_inbound_handler.py:611` `client.send_email()` call site ✓
  • `email_inbound_handler.py:648` `_call_reasoning_engine()` ✓

JSONL contract (post-bucket)

Reasoning-driven send entry shape:
```json
{
"sent_at": "...", "from": "...", "to": [...],
"subject": "...", "in_reply_to": "...",
"send_status": "ok", "message_id": "...",
"smtp_code": 250, "error": null, "retry_count": 0,
"caller_actor_kind": null,
"model_used": "claude-opus-4-7",
"input_tokens": 100, "output_tokens": 50,
"reasoning_duration_ms": 1500,
"caller_session_id": "email:msg-1@example.com"
}
```

Non-reasoning send (MCP `kora__send_email` tool / notification) entry: identical to pre-bucket — the 6 reasoning-meta keys are simply absent (opt-in inclusion).

Test coverage

Handler side (7 new tests):

  • Happy path: full meta threaded (model + tokens + duration + caller_session_id; reasoning_error=None)
  • Engine unavailable: reasoning_error="engine_unavailable", SDK fields None, caller_session_id still derived
  • Engine raises: reasoning_error="engine_exception:RuntimeError"
  • Engine result.error set (e.g., cost_ladder_halted): SDK meta preserved + error code passed through
  • Engine empty text: handler overrides reasoning_error to "empty_response_text" while preserving SDK meta
  • caller_session_id format matches engine's `_derive_caller_session_id`
  • AUTO_REPLY off: meta threading is gated (send_email never called)

Client side (5 new tests):

  • Non-reasoning send: NONE of the 6 new keys appear in JSONL
  • Full reasoning send: all 6 keys land (with opt-in for reasoning_error=None)
  • Partial meta (engine-unavailable): only reasoning_error + caller_session_id appear
  • SMTP failure with reasoning meta: meta still recorded — useful for the panel to show "we tried this model + spent these tokens, then SMTP rejected"
  • `send_email_internal` forwards kwargs

Test plan

  • 12 new tests pass
  • 80 existing tests in touched modules still pass (backwards-compat)
  • 505/505 cross-bucket regression (clients + handlers + test_listeners + reasoning)
  • Ruff clean

Cascade

Unblocks CC#2's KR-REASONING-PANEL-EMAIL-XREF — the panel can now project the new fields into the same xref shape it uses for slack_dm.

🤖 Generated with Claude Code

…email outbound log

Mirrors PR #131's slack_dm post-#131 pattern for the email
outbound path. Closes the gap CC#2 caught via STOP-ASK on
KR-REASONING-PANEL-EMAIL-XREF: ``email_outbound_log.jsonl`` was
missing the model_used / tokens / duration / reasoning_error /
caller_session_id fields that slack_dm's outbound has.

Surface changes (additive + backwards-compat):

  * PurelymailClient.send_email() — 6 new optional kwargs all
    defaulting to None: model_used, input_tokens, output_tokens,
    reasoning_duration_ms, reasoning_error, caller_session_id.
    Threaded through to _append_outbound_log.
  * PurelymailClient._append_outbound_log() — same 6 kwargs.
    Opt-in inclusion in the JSONL entry: each field appears ONLY
    when non-None (mirrors slack_dm_handler.py:817-830 verbatim).
    Non-reasoning callers (MCP send tool, notifications) get the
    pre-bucket entry shape unchanged.
  * send_email_internal() — forwards the 6 kwargs for symmetry.
  * EmailInboundHandler._call_reasoning_engine() — return type
    flipped from (reply_text, reasoning_error) tuple to
    (reply_text, reasoning_meta_dict). The dict has the 5 fields
    PurelymailClient now consumes. Three helpers extracted:
    _empty_reasoning_meta, _reasoning_meta_from_result,
    _email_caller_session_id.
  * EmailInboundHandler._send_auto_reply() — threads the meta
    dict + caller_session_id into client.send_email() via **spread
    + the new named kwarg.
  * EmailInboundHandler._build_reply_and_meta() (NEW) — small
    helper that handles the engine-None / engine-call dispatch
    + uniform meta-dict shape so _send_auto_reply has one code
    path regardless of which failure mode fired.

caller_session_id format documented + enforced consistent with
the reasoning engine's own derivation:

  ``f"email:{message_id}"`` (matches
  ``kora_cli/reasoning/anthropic_engine.py:_derive_caller_session_id``
  line 869-871 for source="email"). Both sides on the same literal
  string lets KR-REASONING-PANEL-EMAIL-XREF join audit ↔ outbound
  JSONL rows by a single field.

A test specifically verifies this by importing the engine's
private helper, constructing an IncomingMessage exactly the way
the handler does, and asserting the handler's derived
caller_session_id equals the engine's output for the same
ParsedIncomingEmail.

12 new tests pass (5 client-side JSONL shape + 7 handler-side
meta threading). 80 existing tests in the touched modules still
pass — backwards-compat preserved across the optional-kwarg
boundary. 505/505 cross-bucket regression (clients/ + handlers/
+ test_listeners/ + reasoning/). Ruff clean.

After this lands CC#2 unblocked on KR-REASONING-PANEL-EMAIL-XREF.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rafe-walker rafe-walker merged commit 43fd931 into feature/phase2-upgrades May 23, 2026
@rafe-walker rafe-walker deleted the feat/kora-KR-EMAIL-OUTBOUND-REASONING-META branch May 23, 2026 20:38
rafe-walker added a commit that referenced this pull request May 23, 2026
…ound (#148)

CC#2 follow-on after CC#1 KR-EMAIL-OUTBOUND-REASONING-META (#146) unblocked the gap her STOP-ASK caught.

- 2 files, +737/-7: extension to reasoning_xref.py (email path: loader + parser + 3-tier matcher) + 20 new email-specific tests.

All 3 K-DG gates verified before drafting per re-dispatch: send_email kwargs ✓; opt-in writer ✓; caller_session_id literal format symmetry between handler + engine ✓.

3-tier cascade: PRIMARY caller_session_id literal equality (closed by #146) → SECONDARY in_reply_to chain → LAST RESORT ±60s timestamp window.

Slack-first precedence preserved: existing #141/#143 tests (42/42) still pass without modification.

response_text carve-out for email-sourced rows: stays null per #124 design (body never in email JSONL); same shape as slack_dm text + #143 message_id carve-outs. Tracked via xref_source local so the conditional null-set cannot regress to populating from a future field rename.

400/400 admin-panel + audit tests pass across 29 suites.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant