- Status: draft
- Date: 2026-02-16
- Scope: ADR 0016 augmentation (internal lifecycle observability only)
Persist precise failed-attempt diagnostics for fresh-call retries so repaired calls remain diagnosable after success.
Primary outcome:
- When a call succeeds on attempt N>1, logs/artifacts still include what failed on attempts 1..N-1.
Non-goals:
- No change to user-facing error text or boundary normalization semantics (ADR 0022 unchanged).
- No category-specific behavior for provenance/external data.
Add internal telemetry fields to fresh-call lifecycle state and emit them on completion:
attempt_failures[](append-only, ordered).latest_failure_stage,latest_failure_class,latest_failure_message.
Each failure record captures:
attempt_idstage(validation,execution,outcome_policy)error_classerror_message(truncated)timestamp(UTC ISO8601)call_id
- Define constants:
MAX_FAILURE_MESSAGE_LENGTH(default 400 chars)MAX_ATTEMPT_FAILURES_RECORDED(default 8 entries per call)
- Define canonical stage values in one place:
validationexecutionoutcome_policy
- Add helper normalizers:
_truncate_failure_message(text)_append_attempt_failure!(state:, attempt_id:, stage:, error:, call_context:)
Acceptance:
- Message truncation is deterministic and UTF-8 safe.
- Unknown stages are rejected or normalized explicitly.
- Extend call state struct/default in
runtimes/ruby/lib/recurgent/call_state.rb:attempt_failureslatest_failure_stagelatest_failure_classlatest_failure_message
- Initialize
attempt_failuresto[]per call. - Ensure fresh-attempt resets do not erase accumulated prior-attempt failures for the same call.
Acceptance:
- Single-attempt success leaves
attempt_failures=[]. - Repaired call retains prior failure records when attempt 2 succeeds.
Capture failures at all three lifecycle stages:
- Validation failures (policy/syntax/guardrail recoverable path).
- Execution failures (exceptions that trigger execution repair).
- Outcome-policy failures (outcome-repair trigger path).
Touchpoints (expected):
runtimes/ruby/lib/recurgent/fresh_generation.rbruntimes/ruby/lib/recurgent/guardrail_policy.rbruntimes/ruby/lib/recurgent/fresh_outcome_repair.rbruntimes/ruby/lib/recurgent/call_execution.rb(ensure/final aggregation path)
Rules:
- Append failure before retry/regeneration.
- Never overwrite previous entries; only append and update
latest_failure_*. - Record same-attempt multi-failure events as separate entries.
Acceptance:
execution_repair_attempts=1implies at least oneattempt_failuresentry with stageexecution.retry_feedback_injected=trueand validation retry implies stagevalidationrecord exists.
- Add fields to log entry mapping in:
- Ensure top-level JSONL includes:
attempt_failureslatest_failure_*
- Enrich artifact generation history metadata on regenerate/repair triggers with:
trigger_error_classtrigger_error_message(truncated)trigger_stagetrigger_attempt_id
Acceptance:
- Repaired successful calls emit both success outcome and failed-attempt diagnostics.
- Artifact history entries for regenerated code include trigger diagnostics.
Add/extend specs in runtimes/ruby/spec/recurgent_spec.rb:
- Validation-first retry test:
- provoke recoverable guardrail violation on attempt 1;
- assert
attempt_failures[0].stage == "validation".
- Execution repair test:
- attempt 1 raises runtime exception, attempt 2 succeeds;
- assert:
execution_repair_attempts == 1attempt_failuresincludes stageexecutionlatest_failure_*matches execution failure.
- Outcome-policy repair test:
- trigger
outcome_repair_attempts; - assert stage
outcome_policyappears inattempt_failures.
- trigger
- Truncation test:
- long error message is truncated to configured limit.
- Boundary safety test:
- user-facing top-level normalized message remains generic (no raw internal error leak).
- Update
docs/observability.mdexamples withattempt_failuresfields. - Capture one fresh repaired trace and include a short snippet in docs (or baseline note).
- Verify
bin/recurgent-watchremains functional with new fields.
Acceptance:
- New fields are documented and visible in live logs.
- Existing watcher filters do not break.
- Ship as additive fields (backward compatible for log consumers).
- Keep defaults conservative:
- bounded message length,
- bounded failure list size.
- No feature flag required unless logs are consumed by strict external parsers.
- Risk: noisy/oversized logs.
- Mitigation: truncation + max-entry cap.
- Risk: accidental user-facing leakage.
- Mitigation: keep emission in internal log/artifact paths only; preserve ADR 0022 boundary behavior.
- Risk: inconsistent stage attribution.
- Mitigation: central stage constants + tests per lane.