# Auto Oncall Dual-Message Response Contract

> Status: Active design and operating contract  
> Updated: 2026-09-01
> Scope: Final Response, persistence, Jira delivery, replay, evaluation, and
> single-ticket optimization

This document is required reading before changing any producer or consumer of
the Auto Oncall customer and internal messages. It owns their end-to-end
lifecycle. Diagnosis, evidence acquisition, and Provider behavior remain owned
by `AUTO_ONCALL_CORE_CONTRACT.md` and are outside this contract.

## 1. One answer, two audiences

Every completed analysis has exactly one canonical response pair:

| Field | Audience | Purpose |
| --- | --- | --- |
| `customerReplyMarkdown` | Customer | The proposed final answer, progress update, or one focused request for customer-owned context. |
| `internalHandoffMarkdown` | L1/Support | The `Internal Only` message containing only useful information deliberately omitted from the customer message, such as evidence boundaries, internal actions, and escalation conditions. The historical field name is retained for wire compatibility and does not imply that another engineer must take over. |

The pair is one answer version, not two independent diagnoses. Both messages
must be non-empty even when the customer message is not eligible for public
delivery. A downstream stage must not regenerate one message from the other or
silently discard either message.

The field names deliberately identify their audience. Producers, schemas,
prompts, APIs, and new persisted result objects use only these two names.
Historical payloads may still be read through the one-way mappings
`commentDraftMarkdown -> customerReplyMarkdown` and
`replayContextMarkdown -> internalHandoffMarkdown`; the old names must not be
emitted as aliases or become a second writable owner. Existing SQL columns may
retain their legacy physical names while the store maps them to the canonical
fields at its boundary.

## 2. Semantic owner and Final Response boundary

The Scenario Router owns the diagnosis and all typed response decisions:

- `decision` and `decisionReason`;
- `requestedContext`;
- `clarificationSimulation.required`;
- the facts, evidence boundary, next owner, action, and escalation condition
  represented in the two messages.

The Final Response Agent receives only that canonical ticket-level decision
and rewrites both messages in one turn. It may improve wording, organization,
language, length, and audience projection. It must not add or remove facts from
the canonical pair, change causal strength, change the requested context,
transfer ownership, choose a different action, or change the typed decision.
Displayed dates, clock times, and timezone labels must refer to the same instant
in both messages. Final Response may convert an unambiguous Router-supplied
time for presentation, including date rollover, but must not merely relabel UTC
as local time, apply the offset twice, or invent a missing source timezone.
The Router continues to own the incident window and its factual correctness.

Keeping an internal operational fact only in `internalHandoffMarkdown` is audience
projection, not removal, when the exact fact remains in the pair and the
customer message preserves the resulting open conclusion and Support-owned
next action.

The shared response-format policy is a presentation input for Primary Analysis
and Final Response, not a second semantic answer owner. It may contain one
customer-versus-PingCAP responsibility presentation rule: when the canonical
analysis has already classified remaining work as Support-owned, do not ask the
customer for the same internally queryable data; put the customer-relevant open
conclusion and Support follow-up in `customerReplyMarkdown`, and put the exact
Support action in `internalHandoffMarkdown`. The format policy must not decide
that ownership, infer Provider capability or failure, choose `human_required`,
or change diagnosis, mitigation, severity, requested context, or approval.

One response-pair language is selected from the latest substantive customer/user
question and applies to both messages. A clearly Chinese question makes both
messages Chinese and a clearly English question makes both messages English.
Every other language, and any question whose language cannot be identified
reliably, defaults both messages to English. Product names, identifiers, field
names, SQL, commands, and other technical literals remain unchanged. Jira
metadata, internal comments, prompt examples, and engineer names do not
override that customer-language signal.

When an answer cites official public documentation, the semantic answer Agent
owns the search, applicability decision, and exact link: Primary Analysis for a
direct answer and Scenario Router reconciliation for the diagnostic pipeline.
That Agent prefers a verified current page in the customer-message language:
Chinese for a Chinese message and English for an English message. Other input
languages default to the English message and English documentation. If no
current same-language page exists, the applicable official page in its
available language may be cited; no Agent may invent a localized URL or rewrite
a locale path without verifying the target page. Final Response preserves the
selected source exactly and performs no documentation lookup. Deterministic
result handling does not search for documentation or maintain locale mappings.

For the diagnostic pipeline, the Scenario Router's canonical response pair
must preserve each explicit customer diagnostic subanswer already resolved at
bounded strength by a Scenario Diagnostic Agent. This includes keeping a
supported optimizer plan-selection explanation distinct from why the observed
execution was slow and whether an alternative plan is proven better. Final
Response may improve that content's wording but cannot restore a subanswer that
the Router omitted.

After Final Response, deterministic handling may parse the structure, enforce
the L1 clarification invariant below, enforce true safety/publication
boundaries, and persist or deliver the pair. It must not run a second wording
or diagnosis Agent.

Runner-owned operational timeout notices may be appended to the existing
Auto-oncall Feishu completion message after the pair is complete. They are not
part of either canonical message, are never supplied to an Agent, do not enter
Jira delivery or evaluation, and do not change the answer fingerprint.

## 3. L1 clarification invariant

Any final customer message that asks the customer to supply information or
take a clarification action requires:

```text
decision = human_required
clarificationSimulation.required = true
requestedContext = the smallest customer-owned discriminator
```

This invariant applies to an explicit request in the actual customer message,
not only to punctuation or classifier intent. A request such as “please
provide”, “please confirm”, or “请补充/确认” is a clarification even without a
question mark. A rhetorical question or a statement of Support's next internal
action is not a customer clarification.

The Router and Final Response Agent must produce consistent typed fields and
text. If structural finalization nevertheless observes a customer request with
`decision=internal_only`, the result is not publication-eligible and the L1
fallback makes it `human_required`. This is an ownership and publication
invariant; it does not authorize deterministic code to invent a diagnosis or a
new customer question.

A classification-time missing key does not by itself require final human
ownership. If authorized internal evidence resolves it and the final customer
message asks no question, the final result may remain `internal_only`.

When Scenario Router applies the Wetech regional SaaS shorthand default owned
by product knowledge, it first preserves useful diagnosis for the assumed
cluster and then asks the customer whether that is the intended cluster.
This final confirmation does not block prior read-only acquisition. It still
requires the three L1 clarification fields above, including when independent
Support-owned query gaps remain. Final Response preserves that already-decided
question; it does not infer the default or invent confirmation itself.
For this bounded confirmation, the safe customer-readable name (for example
`亚太 SaaS02`) may appear in the customer message. Internal region keys,
datasources, endpoints, and machine target references remain internal-only.

## 4. Gap ownership and audience content

The customer message asks only for information that Support cannot retrieve
and that is genuinely customer-owned, such as application behavior, a client
action, or a customer-side observation. It must not ask the customer to provide
Metrics, TopSQL, Slow Query, Statements, component logs, topology, or another
artifact that the selected internal Provider can retrieve merely because an
internal lookup failed, timed out, or returned no row.

The customer message includes the strongest established facts and bounded
conclusion. If work remains internal, it describes Support's next action
without exposing Provider names, internal query errors, or escalation routing.
Because that action is required human handling, the canonical typed decision
must be `human_required`, even when the customer is not being asked for context.
An `internal_only` result may state a non-load-bearing deeper-RCA boundary, but
it must not promise that Support will run another query, check, retry,
follow-up, or escalation.
Every Auto-oncall ticket remains under general human Support oversight; that
background lifecycle ownership is not itself a pending action in an answer.
A replay with
no selected or verified automated Provider capability proves only that
Auto-oncall did not acquire that evidence in the current run; it does not prove
that Support cannot perform the concrete checks in the internal message or
follow up with the customer. A customer-facing statement that Support will
continue those checks and follow up is therefore valid when the paired internal
message gives an actionable Support step. It must not claim that a specific
unperformed operation is already underway.
It does not volunteer that the customer need not provide Metrics or logs.
It also does not expose that internal monitoring or data retrieval failed, that
a target or cluster-to-monitoring mapping was invalid or must be validated, or
the authentication, readiness, parameter, transport, retry, alternative-surface,
owner-routing, or escalation mechanics behind the continuing investigation.
Those details remain in the internal message; the customer sees the exact open
conclusion and that Support will continue internal checks and follow up.

A customer-supplied support-ticket key may appear in the customer message only
when deterministic Jira context acquisition verified that the referenced
ticket shares the current ticket's Jira Service Management organization. The
message may use that key to answer the customer's exact status or progress
question, but it never exposes the referenced Jira URL, reporter, private
title, internal comments, or internal escalation key. Same-reporter fuzzy
candidates remain internal-only. When referenced history contains no recorded
intervention, the pair preserves that bounded fact without rewriting it as
proof of natural recovery; the internal message keeps any Support-owned
confirmation action.

The internal message preserves:

- established facts with entity and window;
- each load-bearing internal lookup as completed, empty, or failed;
- the exact unresolved boundary and its owner;
- the next internal query, retry, or authorized alternative surface;
- the escalation target and trigger;
- any genuinely customer-owned focused question.

For an explicit SQL optimization or tuning request that does not identify the
customer-selected SQL, Digest, or concrete plan and describes no concrete
runtime symptom, the response pair is a pre-investigation clarification. The
customer message asks only which SQL or Digest should be optimized.
`internalHandoffMarkdown` states only that investigation starts after the
customer supplies that target; it must not present global TopSQL or Slow Query
candidates, speculative indexes or rewrites, or completed internal evidence
acquisition.

It preserves the diagnostic semantics of an internal acquisition problem, not
the runner's literal execution vocabulary. `Internal Only` is written for the
Support engineer continuing the ticket and must not contain null field dumps,
internal enum or error identifiers, schema/completion statuses, Agent/Provider
lifecycle labels, machine-form target references, authentication/readiness/
transport state, or retry bookkeeping. Those inputs are translated into the
affected target and window, the technical evidence not yet checked, the
customer decision that remains blocked, the next concrete Support diagnostic
action, and the human-facing team or function to involve if that action remains
blocked. A service or team name appears only as an actionable escalation
destination, not as system-state narration.

When a Wetech route resolved an exact region and cluster and reached Provider
acquisition, `internalHandoffMarkdown` includes that Support-readable identity
as `查询集群：<region> / <cluster>` for Chinese or
`Queried cluster: <region> / <cluster>` for English. The region and cluster
come directly from the Router-owned route anchor. The line stays out of the
customer message and does not expose the machine `wetech:` target-reference
prefix, datasource UID, session, credentials, or routing state.

For example, a target-resolution error is expressed as “the ticket's cluster
name has not yet been tied to the actual cluster, so the alert-window latch
wait and SQL evidence has not been checked; Support should confirm the actual
cluster and rerun those checks.” It is not expressed as a null entity, an
internal error code, a Provider status, or a canonical-target validation step.

When unresolved target identity is the sole blocker and no diagnostic Provider
evidence was acquired, the internal message is shorter still. It contains one
action paragraph with no headings or future query checklist: confirm which
actual cluster the ticket alias refers to, then add an internal Jira comment
using the real ADF `@Auto Oncall` mention and include the actual cluster name so
Auto Oncall regenerates the analysis. Pasted plain-text `@Auto Oncall` is not a
continuation trigger. The blocked technical conclusion stays in the customer
message and is not repeated internally. For example:

```text
请先确认“亚太01”对应的实际集群。确认后，在 Jira 内部评论中选择并 @Auto Oncall，同时注明实际集群名称，重新生成分析。
```

The internal message is the meaningful set difference from the customer
message, not its summary. A conclusion, instruction, example, link, or caveat
already present in `customerReplyMarkdown` is not repeated. Removing that
duplicate from the internal message does not remove a fact from the canonical
pair and is permitted audience projection.

For `decision=internal_only`, when no useful additional internal fact remains,
the internal message is exactly `无额外内部信息。` for a Chinese pair or
`No additional internal-only information.` for an English pair. It does not
repeat the customer conclusion, manufacture a handoff, or narrate absent
comments or attachments, unnecessary diagnostics, or a related-ticket
candidate that was not adopted. Section headings are used only for material
content.
When headings help, a Chinese pair uses `诊断缺口`, `Support 下一步`, and
`需要协助时`; an English pair uses `Diagnostic gap`, `Support next step`, and
`Escalate when`. `Handoff` and `交接` are not generic section headings.

Both messages use Jira-safe Markdown. Inline code must not contain nested or
double backticks. SQL or commands containing backticked identifiers use a
fenced code block; sibling bullets remain contiguous and prose does not share a
line with the closing code fence.

An internal failure or empty result must not be reduced to a generic
“insufficient evidence” conclusion and must not be rewritten as “no data
exists”. The diagnosis and ownership rules that decide these contents remain
in `AUTO_ONCALL_CORE_CONTRACT.md`.

## 5. Canonical lifecycle

```text
Scenario Router canonical decision
  -> Final Response rewrites the pair once
  -> structural/L1/publication invariants
  -> persist both messages as one answer version
  -> replay exposes both messages
  -> Jira delivery preserves the applicable pair
  -> evaluation inspects both audiences
  -> optimizer consumes the paired evidence
```

### 5.1 Persistence and answer identity

One successful run persists both messages, the typed decision,
`requestedContext`, `clarificationSimulation`, execution state, and actual
publication outcome. The stable `answerFingerprint` is calculated from the
normalized customer and internal messages as a pair. Jira wrappers, status
footers, feedback prompts, and run metadata are excluded.

Changing either canonical message creates a new answer version. Reformatting
that normalizes to the same pair does not. Historical single-message
fingerprints remain readable and are not rewritten.

### 5.2 Jira delivery

Live automation remains internal-comment-only unless a separately authorized
customer-publication path admits the customer message. When the customer
message is not written publicly, the stable `public=false` Jira comment
preserves distinct `【建议对客回复】` and `【Internal Only】` sections. `Internal
Only` means content visible only to L1/Support; it is not itself a handoff or a
request for another engineer to take ownership.

For a successfully published `internal_only` final answer, Jira may omit the
redundant internal comment, but the canonical run still retains both messages.
For a published `human_required` progress update, the separate stable internal
comment is mandatory.

### 5.3 Replay and historical write-back

`mode=read` returns both canonical messages and the shared typed envelope
without Jira, DB, or Feishu writes. Historical cutoff and comment filtering
apply to the input; they do not remove either output message.

`mode=write` runs the same analysis and resolves the historical initial owner
job and its stable internal-comment mapping before writing. If that mapping
exists, it updates the exact mapped comment in place. If no mapping exists,
including when the historical answer selected for evaluation is a
customer-visible `public=true` comment, it creates one independent
`public=false` Auto-oncall internal comment and persists the owner-scoped
mapping. The public comment ID is never an internal update target. A
current/default trigger version must not create a second internal comment when
the historical answer used a different trigger version; later write replays
update the newly mapped internal comment. Before recording the new run, write
mode reads the owner's latest successful prior message pair and its
fingerprint-bound feedback. When the new pair has a different fingerprint, the
same Judge receives the prior pair, prior feedback, new pair, and complete
same-stage human answer; an identical fingerprint is not treated as a new
version comparison. Write success requires read-back of that internal comment
ID with `public=false` and the expected paired body.

After a post-deployment `mode=read` passes the single-ticket optimizer's
case-specific criterion, `POST /v1/tickets/publish-read` may promote that exact
frozen result. The caller supplies only the issue key and read run ID. The
service verifies the server-owned artifact and paired-message fingerprint,
then persists those same two messages and the same Judge result under the same
run ID while updating the stable `public=false` Jira internal comment. It does
not rerun analysis, evidence acquisition, final response, or Judge, and it
never accepts either message from the caller. A missing runtime workspace
fails instead of silently starting a new replay.

Continuation uses the same two field meanings and returns a pair. Resuming the
analysis thread must not turn `customerReplyMarkdown` into an internal bot-only
reply or change the audience semantics of either field. This contract does not
require an additional continuation stage or thread.

Continuation also uses the same analysis dispatch as an initial turn. Direct
answers retain Primary Analysis ownership and diagnostics retain Scenario
Router ownership. A persisted thread may be resumed only by the same recorded
role; a role change starts the newly selected role from the trigger-bounded Jira
timeline rather than repurposing the old Agent thread. Staging retains local
Codex session history for 30 rolling days. After that history expires, the
stored thread id is treated as unavailable and the selected role starts a new
thread from the same trigger-bounded canonical Jira timeline.

The Agent9 cutover does not migrate local Codex thread identifiers. When a
continuation job finds a persisted pre-Agent9 thread while the configured
backend is Agent9, it must stop before creating an Agent9 Session, write one
idempotent `public=false` operational notice for that exact source comment, and
finish the job as `continuation_thread_backend_incompatible`. Because no
analysis ran, this notice is not a substitute response pair and must not be
evaluated as a diagnosis. A continuation with no persisted conversation may
start a bounded Agent9 Session, and a persisted `sess_...` conversation may be
resumed normally.

## 6. Evaluation and optimizer consumption

Evaluation treats the pair as one version but checks each audience against its
own purpose:

- Daily/weekly evaluation includes both autonomous entry mechanisms,
  `initial` and `bot_assignment`, while excluding `continuation`. Its concrete
  answer-version anchor is the published Auto-oncall comment: prefer the
  successfully written Public customer reply and fall back to the
  written/updated internal comment. `/ao` may follow either anchor; a command in
  a Public comment is trusted only when the author is identified as Support.
- A report period determines ticket membership from autonomous jobs created in
  that period, then reads the database's current autonomous owner job and latest
  successful run for each member. A historical report refresh must not pin a
  ticket to an older window-local job after a write replay. For the same run,
  a persisted `source=analyze_write` evaluation owns the frozen replay target
  and cannot be overwritten by report-time chronological pairing.
- For the same answer fingerprint, explicit `accept`, `revise`, or `wrong`
  feedback owns the Base quality projection and is never overridden by an LLM
  Judge verdict. A new-fingerprint write Judge may evaluate whether the prior
  feedback problem was fixed, but it must use the prior pair and version-bound
  feedback as inputs and may reject that feedback only with direct frozen-stage
  evidence stated in its comparison.
- Daily comparison gives the shared Judge the complete ordered same-ticket
  Public customer/Support timeline with each comment's frozen-input visibility.
  The Judge selects the minimal later Support comment set that forms the complete
  answer to the original question and compares the customer message with that
  selected answer. It audits the `Internal Only` message separately for missing
  evidence boundary, owner, next action, or escalation; it must not compare the
  `Internal Only` message to the customer's later reply as though they share an
  audience. Owner sufficiency follows the action boundary: `Support` is a valid
  owner for an exact internal query, check, or retry that L1/Support can execute.
  A separate product, component, platform, operations, or approval role is
  required only when the necessary action crosses the Support execution
  boundary. A generic "Support follow up" with no executable action remains a
  gap.
- The same report Judge also receives the typed decision and answer-version
  `/ao` action and reason. Runner-owned execution state remains outside the Judge
  prompt and is available separately to the Optimizer. A paired
  `decision=human_required` answer remains judgeable: later customer facts,
  screen-sharing observations, manual queries, or production operations do not
  make the earlier answer worse unless the earlier answer missed cutoff-visible
  facts, hallucinated, chose the wrong owner, or gave a non-actionable handoff.
  In particular, when the earlier `human_required` pair assigns a concrete
  internal lookup to Support, a later human reply may report that lookup's
  no-match result and only then request customer-owned SQL text or a screenshot.
  The Judge must not treat that later execution result as proof that the earlier
  answer should have skipped the valid internal step and requested the customer
  context immediately. That omission is a gap only when frozen-stage evidence
  already proves the internal lookup was completed and exhausted before the AI
  answer and customer-owned context was the necessary next step.
  The Judge must not invent an internal capability that the frozen input or
  execution state did not establish. `/ao human` requests this judgment rather
  than acting as a binary negative verdict.
- Single-case replay keeps its historical cutoff anchor separate from the
  Judge's human reference. Evaluation, report, and dashboard use the same
  comparison builder and Judge prompt. They provide the full ordered Public
  customer/Support timeline, marking each comment with `visibleToAi`; later
  replies never enter the replay analysis workspace. Code does not stop at the
  next customer reply or classify acknowledgement/substance. The Judge selects
  the smallest chronological set of `role=support && visibleToAi=false` comments
  that completely answers the original question. A customer chase,
  acknowledgement, thanks, repeated question, or unchanged-symptom report does
  not end that search. When a customer adds a new entity, time, symptom, log,
  screenshot, SQL, or execution result, the Judge must keep any dependent later
  conclusion stage-fair. After selecting the human answer, the Judge still reads
  later customer comments for an explicit same-question result confirmation,
  attribution correction, or actual validation. Such comments may validate or
  refute either answer but never enter `targetHumanCommentIds`; unsupported
  guesses, thanks, preferences, or closure requests are not authoritative by
  themselves. Multiple Support replies on the same question form one evolving
  answer. The Judge defines the original question and its explicit requirements
  only from the ticket summary, customer question, and `visibleToAi=true`
  comments. Cause, mechanism, impact, action, and validation are possible answer
  dimensions, not a required checklist; later invisible comments cannot expand
  the original scope. The Judge then retains only the
  last still-valid Support statement for each required dimension. A later
  restatement, completion, correction, or replacement supersedes earlier text;
  earlier content remains only when it is still valid, not restated, and required
  for completeness. Once those dimensions are complete, later requests about
  recovery, another entity, configuration, or risk and their replies stay outside
  the selected answer unless they also correct it. Semantically equivalent
  sets use fewer comments and later valid statements as the deterministic
  tie-break. Selected IDs represent the final answer, not every investigation
  update. It returns `not_compared` when no substantive human answer exists.
  The persisted `referenceComments[]` records only the selected
  comment IDs, timestamps, and body digests; the singular `targetHumanCommentId`
  remains only the replay cutoff anchor.
- When the first Public Support comment is only an acknowledgement or progress
  update and no later Support reply exists, that first comment remains the
  historical cutoff anchor. The analysis workspace includes only the
  reply-safe context before it, while Evaluation ignores the acknowledgement
  as a substantive baseline and normally returns `not_compared`. A new-version
  write that carries prior `wrong|revise` feedback must still judge whether that
  version-bound problem was resolved; an unresolved prior problem yields
  `human_better` even when the current human answer is acknowledgement-only.
  The replay workspace selector must not use an out-of-range comment index or
  switch to the complete current timeline. This does not restrict the separate
  Judge reference timeline.
- The typed decision is the routing fact: `internal_only` means auto-answered,
  no human handling, and no Support-owned next action; `human_required` means
  human handling is required. When prior feedback challenges that routing, the
  Judge must compare the typed decision and must not infer that an
  `internal_only` answer was handed to a human merely because either message
  says Support will continue a query, check, retry, or escalation.
- Recent Evaluation Judge receives both messages plus the typed decision,
  requested context, clarification state, execution state, and publication
  outcome. A correct internal explanation cannot hide an incorrect customer
  request or an `internal_only` clarification.
- A versioned Recent Focus binds the report comparison, run/answer fingerprint,
  replay cutoff, ticket/comment content digests, and exact Judge-selected hidden
  reference comment set. Recent Evaluation replays that bound set; it must not
  silently select a newer human reply for the same issue key. The controller validates
  these digests before Judge execution and invalidates a changed source group
  instead of reinterpreting it as an answer gap.
- Focus/candidate packets may carry the resulting bounded assessment and typed
  fields without copying the raw messages to a patch Agent.
- Single-ticket Optimizer receives both the persisted answer version and the
  fresh replay pair. The fresh single-case Evaluation also returns the exact
  combined same-stage `humanAnswerMarkdown` already supplied to its Judge, with
  `referenceComments[]` retaining the version digests, while the replay response
  returns the paired message `answerFingerprint`. The controller compares the
  nominated and fresh fingerprints and owns `judgeAnswerScope`; the fresh Judge
  verdict remains the current answer's evaluation result. The investigator first
  reviews the nominated pair, that human answer, version-bound feedback, and the
  fresh Judge independently, recording whether the nominated problem is reproduced
  by the current answer; only then does it check the first producer/consumer break across Final
  Response, persistence, replay, evaluation, and delivery. Judge is
  non-authoritative for diagnosis and chain classification. A changed-version
  verdict cannot clear feedback on the nominated historical pair, and that old
  feedback cannot directly overwrite or conflict with the fresh read answer's verdict.
  If verification later reaches `mode=write`, that write Judge receives the old pair
  and feedback explicitly and must decide whether the reported problem was fixed
  before evaluating the new pair. Raw paired messages
  and the human answer stay in the frozen investigation context. Fresh execution also
  supplies the full safe `providerQueryAttempts` sequence; trigger-only deduplicated
  `failureCodes` must not replace that sequence during Provider investigation. These inputs are removed
  before diff admission and never enter a commit.

Historical latest-run payloads may map their legacy internal-message field to
`internalHandoffMarkdown`. They must not manufacture a missing message or claim
that a legacy answer had a verified pair.

## 7. Required invariants and tests

Applicable contract tests must cover:

1. Final Response produces both non-empty messages in one result.
2. An explicit customer information request cannot remain `internal_only` or
   enter the customer-publication candidate path.
3. A resolved classifier-time gap with no final customer request can remain
   `internal_only`.
4. Changing either message changes the answer fingerprint; normalization-only
   changes do not.
5. Persistence and latest-run APIs round-trip both messages and their shared
   typed envelope.
6. `mode=read`, `mode=write`, and continuation preserve the two field meanings.
7. Historical write-back updates the original stable internal comment even
   when its trigger version differs from the current default, without creating
   a duplicate.
8. Daily comparison, Recent Evaluation, and Single-ticket Optimizer consume the
   pair according to section 6.
9. Jira internal delivery rejects an incomplete pair whenever a paired
   internal comment is required.
10. Internal acquisition failures, target mappings, retry mechanics, and
    escalation routing remain in `internalHandoffMarkdown`; the customer message
    preserves only the customer-relevant open conclusion and Support follow-up.
11. New API and artifact outputs expose only `customerReplyMarkdown` and
    `internalHandoffMarkdown`; historical names remain read-only compatibility
    inputs and do not appear as response aliases.
12. Single-case, report, and dashboard Judge inputs use one comparison builder:
    they provide the complete ordered Public customer/Support timeline with
    `visibleToAi`, while the Judge selects the exact Support reference comment
    set. None of those later comments enters the replay analysis workspace. The
    API persists the selected comment versions and exposes the resulting combined
    human answer to the Single-ticket Optimizer so it can review Judge independently;
    the optimizer does not recompute reply selection.

## 8. Change procedure and non-goals

Before changing Final Response, response parsing, answer versioning,
persistence, Jira delivery, replay, continuation, Daily/Recent Evaluation, or
Single-ticket Optimizer response consumption:

1. Read this document, `AUTO_ONCALL_CORE_CONTRACT.md`, and the applicable
   evaluation or optimizer contract.
2. Keep the pair and typed envelope synchronized across every affected
   producer and consumer.
3. Update the applicable contract tests and documentation in the same change.
4. Treat replay preview, Jira write-back, deployment, and case verification as
   separate completion evidence.

This contract does not change scenario selection, diagnosis, Provider routing,
SQL/Slow Query selection, evidence acquisition, customer-publication cohorts,
or production-operation authority. Those changes require their own bounded
design and authorization.
