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 intoMay 23, 2026
Conversation
…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>
3 tasks
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
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:
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):
Client side (5 new tests):
Test plan
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