# SDD: OpenAI-compatible 출력 검증 필터

## 위치

- Milestone: [Milestone 문서](../../../phase/knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md)
- Phase: [PHASE.md](../../../phase/knowledge-tool-optimization-extension/PHASE.md)

## 상태

[승인됨]

## SDD 잠금

- 상태: 해제
- 사용자 리뷰: [user_review_0.log](user_review_0.log) (해결됨)
- 잠금 항목:
  - [x] [D01] assistant history anchor의 preflight history sanitation, live reasoning dedupe, no-progress repair와 온도 단계 복구 정책
  - [x] [D02] `/v1/chat/completions`와 `/v1/responses`를 공통 필터 코어와 endpoint별 codec으로 함께 구현하는 범위
  - [x] [D03] assembled output/reasoning 원문 로그의 설정 기반 기록·중단 정책
  - [x] [D04] provider 오류와 기존 Tool Call Runtime 검증이 공유하는 최초 실행 제외 exact-replay 최대 3회 및 commit 경계 정책
  - [x] [D05] 언어 판별·번역 모델 호출 없이 사용하는 고정 영어 반복 복구 지시문
  - [x] [D06] 오류 사건 집계와 LLM 기반 수정 오케스트레이션을 별도 범용 플랫폼으로 분리하는 책임 경계
  - [x] [D07] provider output-cap `length`를 작은 attempt cap의 managed continuation으로 처리하고, 원본 요청·assistant prefix를 보존한 context-window 기반 논리 trajectory를 fault recovery 최대 3회와 분리하는 정책
  - [x] [D08] filter 승인을 받은 pending tail만 수집 시간 기준으로 비동기 pacing하고, 시간은 release eligibility로 사용하지 않는 정책

## 문제 / 비목표

- 문제: OpenAI-compatible caller가 provider stream에서 반복 출력을 받으면 이미 열린 SSE가 계속 유지되므로 IOP가 caller 제품명과 무관하게 request history와 provider response에서 이상을 감지하고 안전하게 관찰·보정·중단해야 한다. 동일/no-progress tool action은 content 반복과 별도로 tool delta와 history fingerprint를 감시해야 한다. `llama-server` parse error 같은 matched provider 오류는 response-start/status/header/body가 staged된 `transport_uncommitted` 상태에서 bounded lossless request snapshot으로 exact replay해야 하며, status/header/role/body 중 하나가 commit된 stream-open 뒤에는 pending tail이 남아도 exact replay하지 않는다. 단, content 반복의 continuation은 이미 보낸 safe prefix/cursor를 보존해 같은 stream을 이어가는 별도 전략이다. exact replay는 [Tool Call Runtime 검증 재시도 MVP](../../../archive/phase/knowledge-tool-optimization-extension/milestones/tool-call-runtime-validation-retry.md)의 경로/counter를 Core Coordinator가 흡수해 provider 오류와 validation이 최초 실행 제외 최대 3회를 공유하고 filter별 loop를 만들지 않는다. exact/schema와 일반 failure recovery는 Core의 최초 실행 제외 기본값이자 절대 상한 3회 fault cap을 함께 소비한다. provider output-cap `length`는 외부 오류가 아니라 managed profile의 진행 terminal로 판정한다. Core가 이미 release한 safe prefix/cursor를 보존하고 endpoint Rebuilder가 원본 요청과 content/think/reasoning assistant prefix를 channel별로 조립해 같은 stream을 이어 간다. provider가 assistant prefill을 지원하면 우선 사용하고, 아니면 endpoint-private 고정 continuation directive만 더한다. 작은 provider attempt cap은 유지하되 다음 allowance는 original request와 assistant prefix를 조립한 뒤 측정한 `rebuilt_prompt_tokens`에서 reserve를 뺀 실제 context window 및 caller가 명시했다면 남은 논리 output cap으로 제한한다. provider attempt cap은 내부 운영값이고 caller cap은 논리 요청 전체에 한 번만 적용한다. assistant prefix는 rebuilt prompt에 포함되므로 누적 output을 별도 합산하지 않으며, 이 logical trajectory는 fault cap과 별도다. 중간 `length`/`[DONE]`은 caller에게 노출하지 않고 context 여유 또는 caller logical cap 소진 때만 endpoint-native logical `length` terminal과 단일 종료 marker를 전달한다. cancel, complete tool call/side effect 또는 미완성 fragment 실패에서는 endpoint별 final terminal 하나로 수렴한다. 2026-07-16 Pi/Ornith incident는 user가 입력하지 않은 assistant anchor가 assistant reasoning history에 누적돼 user 발화처럼 재인용된 사례이며, generic payload 재구성에서 같은 anchor의 이전 message 11개는 모두 assistant이고 user occurrence는 0이었다. 이를 특정 caller가 아닌 raw HTTP/OpenAI SDK protocol fixture로 일반화한다. `metadata.scheme`은 전체 결과 검증이 필요하므로 hard bound가 있는 terminal gate를 사용한다.
- 문제: rolling evidence threshold를 통과한 pending event를 한 loop에서 모두 release하면 provider가 수집하는 동안의 자연스러운 cadence가 사라지고 threshold마다 짧은 burst와 긴 무응답 구간이 반복된다. 이 문제는 filter 승인 전 hold를 약화하지 않고, 승인된 tail의 전달 lifecycle과 다음 window 수집 lifecycle을 분리해 해결해야 한다.
- 비목표:
  - raw tunnel provider를 normalized RunEvent 실행 경로로 강제 전환하거나 두 path의 raw parser를 합친다.
  - 활성 [OpenAI-compatible API 계약](../../../../agent-contract/outer/openai-compatible-api.md) 밖의 agent·terminal·workspace response protocol을 도입한다.
  - 전체 streaming 응답을 기본적으로 buffer하거나, 경과 시간을 filter 승인 전 release/fail-open 조건으로 사용한다.
  - provider 수집을 멈춘 채 `collect T + deliver T`를 직렬로 수행하거나 synchronous release path 안에서 sleep해 매 window latency를 두 배로 만든다.
  - schema 계약이 있는 요청에서 검증 전 partial content를 성공 출력으로 노출한다.
  - validator 모델로 애매한 자연어/tool-call 후보를 판정한다.
  - Pi session JSONL, Pi SDK 내부 type, Pi local tool invocation을 IOP filter runtime 입력으로 사용한다.
  - 명시적 conversation identity 없이 caller/model 조합이나 history hash를 대화 식별자로 간주해 caller 간 TTL state를 공유한다.
  - Chat Completions와 Responses의 raw request/stream parser, repair request serializer를 하나로 합치거나 한 endpoint의 field를 다른 endpoint에 강제한다.
  - cross-request 오류 사건을 저장·중복 집계하고 연결된 소스 저장소를 분석해 수정 제안·프로젝트 작업 문서·사용자 승인·변경 요청·병합·배포·재발 확인까지 수행하는 범용 플랫폼을 이 Milestone에서 구현한다.

## Source of Truth

| 영역 | 기준 | 메모 |
|------|------|------|
| Roadmap | [Milestone 문서](../../../phase/knowledge-tool-optimization-extension/milestones/openai-compatible-output-validation-filters.md) | 범위, Task, 완료 evidence 기준 |
| Code | `packages/go/streamgate`, `apps/edge/internal/openai`, `apps/edge/internal/service`, `apps/node/internal/adapters/openai_compat` | 공통 gate와 OpenAI endpoint별 codec/rebuilder, Edge dispatcher/release adapter 구현 기준 |
| Implementation Spec | [Stream Evidence Gate 구현 스펙](../../../../agent-spec/runtime/stream-evidence-gate.md) | 현재 구현의 pending/approved/delivered lifecycle, 지원 endpoint와 pacing/terminal/관측 동작을 구현과 같은 변경에서 갱신한다. |
| Contract | [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md) | `metadata.scheme`, 내부 response path, streaming/gated 정책 원문 |
| Config Contract | [edge-config-runtime-refresh.md](../../../../agent-contract/inner/edge-config-runtime-refresh.md) | limit policy의 config field/default/range/refresh 분류는 구현과 함께 갱신하며 active 계약에 미구현 field를 선반영하지 않는다. |
| Stream Mechanics | [Stream Evidence Gate Core SDD](../../../archive/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md) | response-start staging, rolling/terminal/fragment hold, transport commit, recovery와 terminal sequence의 공통 source of truth |
| Incident Evidence | [2026-07-16 Pi/Ornith cross-request history anchor](evidence/2026-07-16-pi-ornith-cross-request-history-anchor.log) | host Pi session과 IOP dev Edge 로그에서 추출한 sanitized 회귀 기준. raw prompt/tool args/result는 포함하지 않는다. |
| Provider Error Evidence | [2026-07-23 llama-server parser error](evidence/2026-07-23-ornith-llama-server-parser-error.log) | `code: 500`, `message: "Failed to parse input at pos"`로 정규화한 provider-error-retry filter 기준. 원문 suffix는 생성 출력이어서 ignored run artifact에만 보관한다. |
| Shared Replay Base | [Tool Call Runtime 검증 재시도 MVP](../../../archive/phase/knowledge-tool-optimization-extension/milestones/tool-call-runtime-validation-retry.md) | 응답 방출 전 bounded exact replay하는 기존 실행 기반. Stream Evidence Gate Core의 RecoveryPlan Coordinator가 이 경로와 attempt counter를 공통 recovery pipeline으로 흡수하고 provider-error/tool-validation filter는 intent만 제공한다. |
| External Provider | OpenAI-compatible provider pool | provider 원본 요청/응답은 IOP 필터 정책에 따라 upstream abort/retry 대상이 된다. |
| User Decision | 현재 사용자 요청 및 [user_review_0.log](user_review_0.log) | caller-neutral OpenAI-compatible filter, 별도 sanitized evidence log, `filters[] = [{ code, message }]`, 기존 Tool Call Runtime validation과의 common exact-replay 재사용, 기본 500-rune evidence pending-tail hold, normalized event/evidence tail/release commit/terminal sequence를 [Stream Evidence Gate Core](../../../archive/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md)의 공통 책임으로 분리하는 결정, D01 history mutation/repair와 `[0.2, 0.4, 0.6]` 온도 단계 복구, D02의 Chat Completions·Responses 동시 적용 및 endpoint별 codec 분리, D03의 기본 원문 기록 `on`과 설정 기반 `off` 전환, D04의 최초 실행 제외 공통 exact-replay 최대 3회와 commit 뒤 no-replay는 확정됐다. 재시도 provider 선택은 기존 provider-pool admission 정책을 따르며 filter가 강제하지 않는다. D05는 언어 판별·번역·로컬 모델 호출을 모두 제거하고 고정 영어 지시문을 직접 사용하는 것으로 확정됐다. 실제 재작업 요청은 반복 구간을 제외한 모델의 content와 think/reasoning 원문을 channel별로 구분해 고정 지시문과 사용하고 원래 사용자 요청·message는 넣지 않는다. D06은 cross-request 오류 사건의 안전한 지문/중복 count, 연결 소스 분석, 중립 수정 제안, 프로젝트별 작업 문서, 사용자 승인, 격리 수정·테스트·독립 검토, 변경 요청·병합·배포·재발 확인을 별도 범용 플랫폼으로 프로젝트화하고 이 Milestone은 raw-free event 방출까지만 맡는 것으로 확정됐다. |
| Diagnostic Smoke Decision | 현재 사용자 요청 | 실제 의미 필터보다 먼저 local/dev 전용 deterministic diagnostic `Filter` mock으로 pass, observe-only violation, pre-release blocking violation과 단일 recovery를 관측한다. mock은 판정만 제어하고 Chat/Responses codec, Core, Arbiter, Recovery Coordinator, ReleaseSink, raw-free observation sink는 실제 구현을 사용한다. 외부 caller가 활성화할 수 없고 production 기본 Registry에는 등록하지 않는다. |
| Failure Contract | [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md), [Stream Evidence Gate Core SDD](../../../archive/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md) | 내부는 최대 4단계의 raw-free `FailureCauseChain`을 보존하고, endpoint host는 HTTP/Chat SSE/Responses에 endpoint별 단일 terminal 오류만 직렬화한다. |
| Managed Length Decision | 현재 사용자 요청 | managed profile은 provider의 작은 attempt `max_tokens`를 사용한다. output-cap `length`는 중간 terminal로 내보내지 않고 original request와 channel별 assistant prefix로 다음 attempt를 rebuild하며, original request와 assistant prefix를 조립한 뒤 측정한 rebuilt prompt token 및 reserve로 계산한 context-window 여유가 논리 trajectory의 유일한 장문 상한이다. prefix는 rebuilt prompt에 이미 포함되므로 별도의 누적 output과 이중 계상하지 않는다. fault recovery 3회 cap과 별도다. caller가 명시한 output cap은 provider attempt cap으로 취급하지 않고 논리 요청 전체에 한 번만 적용한다. |
| Approved Tail Delivery Decision | 현재 사용자 요청 | rolling window의 first eligible text/reasoning delta부터 evidence threshold 또는 terminal까지 수집 시간 `T`를 측정한다. all-complete filter pass를 받은 payload만 immutable approved queue로 이동하고, 다음 provider window 수집과 겹쳐 `T` 동안 rune-weighted frame pacing한다. 시간은 release eligibility가 아니며 recovery는 unapproved tail만 폐기·교체한다. |

## State Machine

| 상태 | 진입 조건 | 다음 상태 | 근거 |
|------|-----------|-----------|------|
| `ingress` | `/v1/chat/completions` 또는 `/v1/responses` 요청 수신 | `repeat_guard_preflight`, `passthrough`, `guarded_stream`, `contract_schema`, `policy_rejected` | endpoint host가 body를 읽기 전에 기본값/절대 상한 16 MiB를 적용하고 raw-canonical bounded snapshot과 endpoint history, registry/config generation, 기본값/절대 상한 3회 recovery cap 및 required filter capability를 고정한다. initial overflow는 dispatch 전 413이다 |
| `repeat_guard_preflight` | endpoint codec이 repeated action이나 assistant-only history anchor 후보를 common filter input으로 전달한다 | `history_sanitizing`, `guard_error`, `guarded_stream` | Chat messages 또는 Responses input item을 role-separated text/action fingerprint history event로 해석하되 raw field parser는 공유하지 않는다 |
| `history_sanitizing` | D01에서 preflight mutation이 승인됐고 plain assistant reasoning history가 mutation threshold를 넘겼다 | `guarded_stream`, `guard_error` | signed/encrypted/tool/content를 제외한 bounded request-history sanitation |
| `passthrough` | `metadata.scheme` 없음, guarded filter 비활성 또는 적용 대상 아님 | `done`, `provider_error_retry`, `provider_error` | filterable path에서는 provider response-start/opening event도 첫 safe release 전 stage하고, pure passthrough는 필수 filter가 전혀 없는 명시적 policy에서만 사용한다 |
| `provider_error_retry` | provider 오류의 `code` exact-match와 `message` 포함-match가 같은 `filters[]` 원소에 맞고 Core `CommitState=transport_uncommitted`다 | `exact_replay`, `provider_error` | filter는 `exact_replay` intent만 반환한다. staged response-start나 pending이 있어도 user-visible commit이 없으면 eligible이고 stream-open이면 들어가지 않는다 |
| `exact_replay` | Core Arbiter가 provider error 또는 Tool Call validation intent를 하나의 recovery 후보로 선택했다 | `passthrough`, `guarded_stream`, `provider_error` | RecoveryPlan Coordinator가 최초 실행 제외 공통 최대 3회 exact budget, 모든 strategy를 합산한 기본값/절대 상한 3회 request 전체 cap과 commit/cancel/side-effect를 확인하고 current attempt abort 뒤 bounded raw-canonical Rebuilder와 기존 provider-pool admission으로 cycle당 outbound attempt 하나를 수행한다 |
| `provider_error` | 오류가 retry 불가, 공통 3회 budget 소진 또는 stream-open 뒤 발생했다 | `done` | uncommitted이면 staged provider start를 버리고 caller-facing HTTP error, stream-open이면 terminal SSE error/close; exact replay 금지 |
| `guarded_stream` | streaming 응답에 repeat/history/action/provider-error filter 중 active set 적용 | `observe_continue`, `dedupe_continue`, `approved_tail`, `repairing`, `provider_error_retry`, `done`, `guard_error` | Core가 response-start와 rolling/fragment evidence를 보류하고 same immutable batch를 single-flight 병렬 평가한다. all-complete 뒤 하나의 action만 선택한다 |
| `observe_continue` | guard가 후보를 감지했지만 D01이 observe-only이거나 판정 근거가 부족하다 | `guarded_stream`, `done`, `guard_error` | response mutation 없는 observation |
| `dedupe_continue` | D01에서 live dedupe가 승인됐고 assistant-only anchor가 plain non-final reasoning에 재등장했다 | `guarded_stream`, `done`, `guard_error` | bounded candidate fragment suppression과 `assistant_history_anchor` observation |
| `approved_tail` | rolling epoch의 active filter outcome이 all-complete pass이고 eligible text/reasoning delta가 threshold 또는 terminal에 도달했다 | `guarded_stream`, `paced_delivery`, `guard_error` | first eligible delta부터 threshold/terminal까지의 monotonic 수집 시간 `T`, approved rune 수와 ordered endpoint event를 immutable queue item으로 고정한다. prefill/filter 평가 시간은 제외하고 recovery는 이 상태 이후 payload를 폐기·교체하지 않는다 |
| `paced_delivery` | approved queue가 비어 있지 않다 | `paced_delivery`, `done`, `guard_error` | 비동기 scheduler가 frame tick별 누적 rune quota로 approved text/reasoning delta를 `T` 동안 전달한다. 다음 provider window는 동시에 `guarded_stream`에서 수집·평가하며 queue bound에 도달할 때만 upstream backpressure를 적용한다. structural/tool event는 원자적으로 순서를 보존하고 terminal은 queue drain 뒤 한 번만 전달한다 |
| `repairing` | single-stream content 반복 또는 D01에서 승인된 무진전 assistant-history 반복을 filter가 감지했다 | `repair_prompt_preparing`, `guard_error` | filter는 반복 전 원문 보존·반복 구간 제외와 다음 온도 후보를 typed directive로 가진 `continuation_repair` RecoveryIntent를 반환하고 직접 abort/retry하지 않는다. Core는 all-complete 뒤 plan 하나를 고르고 current attempt ownership을 먼저 종료한다 |
| `repair_prompt_preparing` | Core Arbiter가 continuation intent를 선택했고 terminal/tool side effect·문맥 한도 때문에 차단되지 않았다 | `guarded_stream`, `guard_error` | endpoint Rebuilder가 반복 전 content와 think/reasoning 원문을 channel별로 구분하고 고정 영어 지시문을 더해 새 요청을 직접 조립한다. 사용자 요청·message, 언어 판별·번역·별도 모델 호출은 포함하지 않는다. Core는 stream-open safe prefix/look-behind/release cursor를 보존하고 이미 종료한 current attempt를 다시 abort하지 않으며, 새 response-start/role/기존 prefix는 중복 release하지 않는다 |
| `length_continuation` | codec이 provider output-cap `length` terminal을 전달하고 `provider_length_gate`가 managed profile·context trajectory·tool/cancel 경계를 통과시켰다 | `guarded_stream`, `length_terminal` | Core는 중간 terminal을 commit하지 않고 safe prefix/cursor를 보존한다. current attempt ownership 종료 뒤 endpoint Rebuilder가 original request와 channel별 assistant prefix를 조립하고, prefill 지원 시 우선 사용해 다음 small-cap attempt 하나를 dispatch한다 |
| `length_terminal` | context window 또는 caller logical cap allowance가 없거나 complete tool call·side effect·cancel·lossless fragment serialization 실패가 발생했다 | `done` | context/caller logical cap 소진이면 중간 provider `length`가 아닌 endpoint-native logical `length` terminal과 종료 marker를 한 번 전송한다. 그 밖의 경계 실패는 endpoint별 final terminal 하나로 전송한다 |
| `contract_schema` | `metadata.scheme` 있음 | `schema_retry`, `done`, `schema_error` | content channel bounded `terminal_gate`; staged response-start/content를 검증 전 release하지 않고 hard bound 초과는 fail-closed |
| `schema_retry` | schema filter가 validation failure와 typed repair directive를 반환했다 | `contract_schema`, `schema_error` | 공통 Coordinator가 schema strategy budget, request 전체 recovery cap과 rebuilt request byte limit을 확인하고 RequestRebuilder로 하나의 새 attempt를 만든다 |
| `policy_rejected` | caller가 요청한 필수 filter capability를 만족하는 provider 후보가 없거나 config에서 비활성 | `done` | provider dispatch 및 response commit 전 OpenAI-compatible invalid_request_error |
| `guard_error` | repair 불가, retry exhausted, 문맥 한도 초과, tool side-effect 구간 진입, side-effect 가능 action 반복 감지 | `done` | Core가 bounded `FailureCauseChain`과 public error descriptor를 `ReleaseSink`에 전달하고 endpoint별 OpenAI-compatible terminal 오류 하나만 보낸다 |
| `schema_error` | retry 후에도 JSON/schema 불일치 | `done` | OpenAI-compatible validation error |
| `done` | 성공 응답 또는 terminal error 전송 | 없음 | final chunk/body |

## Interface Contract

- 계약 원문: [openai-compatible-api.md](../../../../agent-contract/outer/openai-compatible-api.md)
- 입력:
  - `metadata.scheme`: optional JSON schema object. 있으면 IOP는 요청 출력 계약으로 간주한다.
  - `stream`: caller 요청값. `metadata.scheme`이 있으면 downstream content streaming보다 schema validation이 우선한다.
  - `messages`: 마지막 user message에 IOP output scheme instruction을 append할 대상이며, role별 user/assistant text provenance와 이전 assistant tool call/tool result를 이용해 history anchor 및 action 반복 context를 판정하는 현재 request source of truth다.
  - Responses `input`: endpoint codec이 user/assistant text, reasoning, function-call, completed result provenance를 보존해 common history/action context로 변환할 대상이다. Chat `messages`의 field나 parser를 그대로 사용하지 않으며, encrypted/unknown reasoning item은 D01 sanitation 대상이 아니다.
  - `tools`: tool name/argument 구조와 side effect 안전성 판단에 사용한다.
  - ingress request snapshot: Edge가 body를 읽기 전에 Core의 기본값/절대 상한 16 MiB `max_ingress_snapshot_bytes`를 적용해 보존한 request-local OpenAI JSON raw body 하나와 bounded typed semantic view다. `retained_bytes`는 canonical raw body, typed view와 rebuild 임시 output을 합산하고 unknown caller field/item을 exact replay와 typed repair에서 보존한다. Core Coordinator와 endpoint `RequestRebuilder`만 사용하며 initial overflow는 413, rebuild overflow는 commit state에 맞는 terminal error로 dispatch/recovery budget 소비 전에 fail-closed한다. filter/로그/cross-request cache에는 노출하지 않고 raw authorization/provider-auth header는 포함하지 않는다.
  - provider error retry `filters[]`: 각 원소는 `code`와 `message`만 가진다. `code`는 provider error code와 exact-match, `message`는 오류 메시지와 Unicode 포함-match한다. 한 원소라도 두 조건을 만족하면 `provider_error_filter`가 `exact_replay` RecoveryIntent를 반환하며, 원소 순서나 중복은 의미가 없다. 초기 원소는 `{ code: 500, message: "Failed to parse input at pos" }`다. position 숫자와 그 뒤의 생성 출력은 filter 값으로 저장하지 않는다.
  - provider length terminal: codec은 `finish_reason=length` 또는 동등한 provider output-cap terminal reason을 raw body 없이 `provider_length_gate`에 전달한다. managed profile에서만 filter가 `managed_continuation` intent를 반환하며, Core의 immutable `ManagedTrajectoryBudget`은 attempt cap·context window·Rebuilder가 측정한 `rebuilt_prompt_tokens`·reserve와 optional caller logical output cap으로 다음 allowance를 계산하며, prefix를 누적 output으로 다시 합산하지 않는다. 이는 error retry counter와 별도이고 provider별 assistant prefill/continue-final-message capability는 endpoint Rebuilder가 선택한다.
- 출력:
  - `passthrough`: 기존 provider-compatible 응답을 유지한다.
  - `passthrough_guarded`: Core `rolling_window`/`fragment_gate` 위에서 repeat/history/action filter가 observe, bounded dedupe, violation/fatal/replacement 또는 `continuation_repair` intent를 반환한다. 모든 active outcome 전에는 response-start/role/content를 release하지 않고, stream-open continuation은 기존 safe prefix를 보존한 채 새 attempt opening/prefix를 중복하지 않는다.
  - `approved_tail_delivery`: `passthrough_guarded`의 rolling window가 all-complete pass한 뒤 text/reasoning payload만 `pending`에서 immutable `approved` queue로 이동한다. scheduler는 수집 시간 `T`와 rune 수로 계산한 frame별 누적 quota만큼 endpoint-safe delta를 비동기 전달하며 structural/tool event는 분할하지 않는다. 다음 window 수집은 delivery와 겹치고 terminal은 queue drain 뒤 한 번만 전달된다.
  - `provider_error_retry`: `filters[]` matched error가 Core `CommitState=transport_uncommitted`일 때만 `exact_replay` intent를 반환한다. staged status/header/body는 commit이 아니며 Coordinator가 Tool Call validation과 최초 실행 제외 최대 3회를 공유하고 current attempt abort 뒤 lossless Rebuilder/provider-pool admission을 cycle마다 한 번 호출한다. stream-open 뒤에는 pending 유무와 관계없이 만들지 않는다.
  - `contract_schema`: content channel을 configured hard bound의 `terminal_gate`로 수집/검증한 뒤 valid JSON만 반환한다. invalid는 `schema_repair` intent, overflow는 partial release 없는 terminal error이며 `stream=true`에서도 response-start/`delta.content`를 검증 전에 commit하지 않는다.
  - `managed_length_continuation`: 중간 `length`/`[DONE]`을 caller에 보내지 않고 same-stream safe prefix/cursor를 보존한다. original request와 content/think/reasoning assistant prefix를 lossless endpoint shape로 rebuild하며, prefill 불가 시 endpoint-private 고정 directive만 사용하고 새 user message·요약·문장 경계 절단·별도 모델 호출은 만들지 않는다. 미완성 tool fragment는 endpoint Rebuilder가 assistant continuation prefix로 lossless하게 직렬화할 수 있을 때만 내부에 포함하며, 그렇지 않으면 release 없이 최종 오류로 끝낸다. context 또는 caller logical cap으로 trajectory가 끝날 때만 endpoint-native logical `length` terminal과 종료 marker를 한 번 보내며, tool boundary가 닫히면 endpoint별 final terminal 하나만 보낸다. attempt 중간 `length`는 보내지 않는다.
  - `tool_validation_error`, `schema_validation_error`, `guard_error`: 복구 불가 또는 retry exhausted terminal error.
- 내부 filter interface/policy:
  - 구현은 [Stream Evidence Gate Core](../../../archive/sdd/knowledge-tool-optimization-extension/stream-evidence-gate-core/SDD.md)의 Go `Filter` interface와 shared helper를 사용한다. 각 filter는 stable ID, applicability, hold requirement, context-aware synchronous pure evaluation, sanitized evidence와 선택적 RecoveryIntent만 제공하며 error/cancel/deadline은 Core fail policy로 전달한다.
  - diagnostic `Filter` mock은 local/dev smoke의 명시적 test seam에서만 등록하고 deterministic decision만 반환한다. 외부 request/config field로 선택할 수 없고 production 기본 Registry에는 포함하지 않으며, codec/Core/Arbiter/recovery/ReleaseSink/observation sink는 mock으로 대체하지 않는다.
  - repeat/schema/provider-error처럼 사용자 출력 안전성에 직접 관여하는 filter는 기본 `blocking`, dev-corp 사전 관찰용 action rule은 명시적 `observe_only`로 등록할 수 있다. blocking error/deadline은 fatal, observe-only error/deadline은 `observe_error`로 정규화하며 어느 경우에도 silent pass하지 않는다.
  - Chat/Responses codec은 response-start를 포함한 normalized event와 lossless `RequestRebuilder`를 제공하고 Edge adapter는 Core `AttemptDispatcher`/`AttemptController`/`ReleaseSink`를 구현한다. Core는 hold, all-complete, commit, recovery budget/abort/rebuild 호출/dispatch를 담당하고 filter는 반복/schema/provider-error 의미와 typed intent만 담당한다.
  - 기본 repeat는 `stream_hold.evidence_runes=500` rolling window, schema는 explicit bounded terminal gate, tool fragment는 fragment gate다. terminal gate 외 기본 경로는 전체 응답을 모으지 않으며 response status/header와 opening role/event도 첫 safe release까지 stage한다. 시간 경과는 release eligibility가 아니며 idle evidence 미충족은 계속 fail-closed한다.
  - rolling release lifecycle은 `pending -> approved -> delivered`다. Core는 첫 eligible text/reasoning delta부터 threshold/terminal까지 monotonic 수집 시간 `T`를 재고 all-complete pass 뒤에만 immutable approved queue item을 만든다. delivery scheduler는 별도 비동기 ownership에서 고정 frame tick과 누적 rune quota로 `T`에 맞춰 endpoint-safe text/reasoning event를 나누고, 다음 window collection/filter evaluation은 동시에 진행한다. bounded queue가 찰 때만 upstream을 backpressure하며 prefill/filter latency는 `T`에 포함하지 않는다. recovery는 unapproved tail만 폐기·교체할 수 있고 approved item의 payload/order/duration은 변경하지 않는다.
  - stream consumer filter는 Core의 `FilterObservation` timeline에 stable `consumer_id`/`filter_id`/`rule_id`, effective/pending rune, decision, commit/terminal state와 sanitized fingerprint/count/offset evidence, 최대 4개의 sanitized failure cause code만 남긴다. request/run/provider/model correlation은 Core가 전파하며 raw output/prompt/tool args/result/auth, raw stack trace, provider endpoint/body는 넣지 않는다.
  - `/v1/chat/completions`와 `/v1/responses`는 이번 Milestone 범위에 포함한다. Chat Completions는 message/delta/tool-call parser와 Chat `RequestRebuilder`를, Responses는 item/reasoning/function-call parser와 Responses `RequestRebuilder`를 각각 제공하며, Coordinator가 동일 filter decision/intent에서 endpoint별 구현을 호출한다. Claude stream과 agent-family별 codec은 이번 범위에 포함하지 않는다.
  - filter input과 decision에는 caller/agent 제품명을 넣지 않는다. 동일한 OpenAI-compatible payload와 provider capability는 raw HTTP, OpenAI SDK, Pi 등 caller가 달라도 같은 판정을 내린다.
  - 모든 filter는 filter id, feature name, 지원 response path, 기본 활성값, request 적용 여부, 실행/검증 결과를 같은 interface로 노출한다.
  - `FilterContext`는 request-local config generation, attempt/epoch, environment, model group/actual model, provider id/type, execution path, endpoint, stream/commit/side-effect와 required feature를 포함한다.
  - request 시작 시 Registry/config generation을 고정하고 required feature를 provider admission constraint로 전달한다. enablement는 같은 snapshot에서 feature별로 평가하며 우선순위는 `environment + model/provider + feature`, `model/provider + feature`, `environment + feature`, 기본값 순서다. provider/path가 바뀐 attempt는 actual target 기준으로 다시 resolve한다.
  - 반복루프 guard 같은 optional online filter가 비활성화되면 해당 filter만 skip하고 pure passthrough 또는 남은 filter path로 진행한다.
  - `metadata.scheme`처럼 caller가 필수 출력 계약을 요청한 filter가 비활성화되면 silent passthrough로 낮추지 않고 `policy_rejected`로 종료한다.
  - action 반복 guard는 `tool name + normalized args`를 안정 fingerprint로 만들고, 현재 incoming `messages` 안의 tool call/result, 현재 provider tool call delta, consecutive 동일 fingerprint, 동일/no-progress tool result, threshold를 함께 만족할 때만 repeated action으로 판정한다. 명시적 conversation identity가 추가되기 전에는 request 외 TTL/session state를 판정에 사용하지 않는다.
  - provider error filter는 `code` exact-match와 `message` 포함-match가 같은 원소에 맞고 Core `CommitState=transport_uncommitted`일 때만 `exact_replay` intent를 반환한다. response-start/status/header와 body가 staged 상태면 eligible이지만 `ReleaseSink`가 status/header/role/body 중 하나를 commit한 뒤에는 pending이 남아도 exact replay하지 않는다. Core가 D04 공통 최대 3회, continuation/schema strategy budget과 이들을 모두 합산하는 최초 실행 제외 기본값/절대 상한 3회의 request 전체 `max_recovery_attempts_total`, abort-before-dispatch, cancel/side-effect/re-entry guard를 관리한다. policy는 0~3만 허용하고 4 이상은 config validation error다.
  - tool call delta hold는 full call fingerprint 판정에 필요한 최소 fragment에만 적용하며, content delta 전체 buffering으로 확장하지 않는다. 판정이 safe이면 held tool call delta를 release하고, repeated action이면 downstream으로 release하지 않는다.
  - 현재 계약에는 caller-neutral conversation identity가 없으므로 MVP는 incoming `messages`와 current response만 사용한다. request history만으로 반복 여부를 안정적으로 판정할 수 없으면 guard mode로 단정하지 않고 current-stream 검사 또는 observe-only로 낮춘다.
  - action 반복 observation은 filter id, feature name, model/provider, fingerprint hash, action 종류, repeat count, threshold, decision을 남기되 Prometheus label은 낮은 cardinality 값으로 제한한다.
  - assistant history anchor inspector는 Chat Completions payload에서는 `content`, `reasoning_content`, `reasoning`, `reasoning_text`를 provider reasoning alias registry로 읽고, Responses payload에서는 endpoint codec이 role/channel을 보존한 text/reasoning item으로 변환한 입력만 읽는다. 두 codec은 whitespace/control normalization 뒤 fingerprint를 만들고, 같은 fingerprint가 user 입력에는 없으며 현재 endpoint history의 assistant 항목에 configurable N회 누적됐을 때 후보로 판정한다.
  - caller가 reasoning history를 재전송하지 않으면 해당 channel의 과거 occurrence를 0으로 본다. 명시 conversation identity 계약이 없는 상태에서 history-lineage hash나 configured OpenAI session id로 누락 occurrence를 보충하지 않는다. provider 전환은 현재 request history에 들어 있는 channel alias와 current response를 통해서만 관찰한다.
  - progress는 validator 모델 없이 incoming history에서 완료된 이전 tool/action fingerprint, tool result/error hash 변화, terminal 상태만으로 판정한다. current response의 tool result는 아직 존재하지 않으므로 progress 근거가 아니며, 서로 다른 action 자체만으로 진전을 단정하지 않는다.
  - history sanitation은 D01 승인 시 plain string reasoning의 반복 fragment에만 적용한다. assistant final `content`, tool call, signed/encrypted reasoning, 알 수 없는 provider field는 변경하지 않으며, repair request도 같은 sanitized history를 사용해야 한다.
  - live reasoning dedupe와 no-progress repair는 D01 승인 결과를 따른다. tool call이 이미 release됐거나 side effect 가능 구간이면 자동 repair하지 않고 안전 중단 정책을 따른다.
  - D01 filter는 반복 전 모델 응답 content와 think/reasoning 출력 원문에서 반복 구간만 제외한 typed continuation directive를 만든다. 복구용 새 요청은 두 channel의 모델 출력과 고정 복구 지시문만 사용하고 원래 사용자 요청·message는 넣지 않는다. channel provenance를 유지하고 모델 출력은 의미 요약·임의 절단·재작성하지 않으며 전체가 문맥 한도를 넘으면 자동 복구하지 않는다. caller가 `temperature`를 명시하지 않은 경우 `[0.2, 0.4, 0.6]` 후보를 intent에 제공하며, Core Coordinator가 continuation strategy budget, request 전체 recovery cap과 다음 후보 선택, RequestRebuilder 적용, single re-admission을 소유한다. 배열 또는 전체 cap 소진은 `guard_error`로 끝낸다.
  - D05 복구 지시문은 `The previous model output was stopped after a repetition loop was detected. Continue from the provided content and reasoning output without repeating already generated text.`로 고정한다. 언어 판별·번역·로컬 모델 호출·별도 준비 재시도는 사용하지 않는다. all-complete Arbiter가 continuation plan을 선택하고 current provider transport ownership을 종료한 뒤 endpoint Rebuilder가 반복 전 content와 think/reasoning 원문을 channel별로 구분해 고정 지시문과 직접 조립한다. 사용자 요청·message는 넣지 않으며, 복구 budget은 실제 outbound recovery dispatch에서만 소비한다.
  - D03은 assembled output/reasoning 원문 기록을 설정 기본값 `on`으로 둔다. 설정이 `off`이면 이후 요청부터 원문을 쓰지 않고 반복/재시도/중단 판단, 길이, role/channel, model/provider 같은 비원문 운영 정보만 남긴다. raw user prompt, tool args/result, 인증 정보는 설정값과 무관하게 기록하지 않으며, `off` 전환이 기존 로그를 자동 삭제하지는 않는다.
  - history anchor observation은 fingerprint hash, first-seen role/channel, current role/channel, request-history count, provider transition, completed-progress signal, decision만 남긴다. output/reasoning 원문 기록은 D03 설정을 따르고 raw message/tool payload는 기본 filter metric label에 넣지 않는다.
  - Responses item/reasoning/tool 계약을 Chat Completions parser로 처리하지 않는다. endpoint별 codec이 같은 normalized event와 repair input을 만들며, 공통 filter core는 endpoint 이름이나 raw event shape에 의존하지 않는다.
- 내부 path 주의:
  - `passthrough_guarded`와 `contract_schema`는 caller가 지정하는 공개 request field가 아니라 IOP 내부 실행/로그 path 이름이다.
  - 공개 OpenAI-compatible 경로 선택은 [계약 원문](../../../../agent-contract/outer/openai-compatible-api.md)에 따라 request `model`이 가리키는 provider capability로 결정한다.
- 금지:
  - `metadata.scheme`을 `iop_output_contract` 같은 wrapper로 감싸지 않는다.
  - schema 계약 요청을 normalized path로 보내지 않는다.
  - provider-pool이 선택한 tunnel/normalized 실행 path를 filter가 바꾸거나 raw parser를 공유하지 않는다. 두 path가 codec 뒤 같은 Core event를 쓰는 것은 허용한다.
  - 반복루프 repair 안내 문구를 assistant content chunk로 주입하지 않는다.
  - 이미 downstream으로 tool call을 보내 실행 side effect 가능성이 생긴 뒤 자동 continuation repair를 수행하지 않는다.
  - 승인 전 pending payload를 timer 만료로 release하거나, synchronous `ReleaseSafe`/sink call 안에서 sleep해 provider 수집을 직렬로 막지 않는다.
  - structural event나 tool-call JSON fragment를 pacing용 rune chunk로 분할하지 않고, approved queue drain 전에 terminal 또는 `[DONE]`을 전달하지 않는다.
  - tool/action fingerprint raw args나 secret 가능 문자열을 metric label, request id, provider id 같은 장기 식별자에 넣지 않는다.
  - side-effect 가능 tool/action을 자동 replay하거나 continuation repair로 재실행하지 않는다.
  - assistant history anchor 증거를 위해 raw prompt, raw tool args/result, 전체 reasoning/content를 장기 로그에 복제하지 않는다.
  - 내부 failure cause를 외부 오류의 `causes`, `stack`, `trace` 필드로 내보내거나 raw stack trace, provider endpoint/body, 원문 출력·입력으로 구성하지 않는다.
  - 진행 신호가 있는 서로 다른 tool/action을 assistant history anchor 반복만으로 차단하지 않는다.
  - assistant final `content`를 history anchor fingerprint만으로 조용히 삭제하지 않는다.
  - caller/agent 제품명으로 filter enablement, threshold, state key, repair path를 분기하지 않는다.
  - Pi session 파일이나 caller SDK 내부 message object를 읽어 runtime 판정을 보강하지 않는다.

## Acceptance Scenarios

| ID | Milestone Task | Given | When | Then |
|----|----------------|-------|------|------|
| S01 | `contract-doc` | OpenAI-compatible Chat Completions·Responses 계약 문서와 구현 타입 | `metadata.scheme`, caller-neutral guard 및 D02 endpoint 범위가 구현된다 | 구현과 같은 변경에서 outer API·inner config 계약과 handler/config type이 wrapper 없는 `metadata.scheme`, tunnel/normalized RunEvent 경계, 기본값/절대 상한 16 MiB pre-read raw-canonical snapshot, initial 413와 rebuild commit-aware terminal 오류, conversation identity 부재 시 degradation을 설명하고 raw parser/serializer는 path·endpoint별로 분리한다 |
| S02 | `filter-pipeline` | provider route 요청이 들어온다 | request snapshot과 filter policy를 평가한다 | repeat rolling, schema terminal, tool fragment hold와 blocking/observe enforcement가 resolve되고 required unsupported 조합은 admission 전 400으로 거부된다 |
| S03 | `repeat-guard` | provider SSE가 UTF-8 multi-byte 경계로 분할된 긴 한국어 문단 6개를 다시 반복 출력한다 | 500-rune rolling pending과 committed look-behind가 candidate를 검사한다 | current upstream을 abort하고 이미 보낸 safe prefix/cursor를 보존한 continuation attempt를 같은 downstream SSE에 이어 붙인다. 반복 구간과 새 response-start/role/기존 prefix는 제외하고 `[0.2, 0.4, 0.6]` 후보를 순서대로 시도하며 `[DONE]`은 한 번만 출력한다 |
| S04 | `repeat-guard` | tool call delta가 이미 downstream으로 흘렀거나 tool side effect 가능 구간이다 | content 반복 또는 action fingerprint 반복이 감지된다 | 자동 repair를 수행하지 않고 guard terminal error 또는 안전한 중단으로 끝낸다 |
| S05 | `schema-contract` | request `metadata.scheme`에 JSON schema와 terminal-gate hard bound가 있다 | provider가 bound 안의 schema-valid JSON을 반환한다 | response-start/content를 검증 전 commit하지 않고 JSON validation 뒤 valid JSON만 반환한다 |
| S06 | `schema-contract` | request `metadata.scheme`에 JSON schema가 있다 | provider가 invalid JSON 또는 schema 위반 JSON을 반환한다 | schema filter가 typed intent를 반환하고 Core가 uncommitted 상태에서 lossless Rebuilder로 새 attempt를 만들며 unknown request field를 보존하고 반복 실패는 schema error로 끝난다 |
| S07 | `ops-evidence` | generic raw HTTP/OpenAI SDK에서 반복루프 또는 schema 계약 smoke를 실행하고, dev `ornith:35b`에서 `stream=true` 긴 한국어 출력 요청을 model group 총 capacity+1 동시 요청으로 최소 3회 실행하며 Pi TUI는 선택적 field smoke로 추가한다 | assembled output/reasoning 원문 기록 설정의 `on`과 `off`를 각각 적용한다 | `on`일 때는 assembled output/reasoning 원문을 기록하고, `off` 뒤 요청에는 원문 없이 비원문 운영 정보만 기록한다. raw user prompt/tool args/result/auth는 어느 설정에서도 기록하지 않으며 caller 제품명과 무관하게 문제 축이 model/provider, IOP guard, protocol 경계로 구분된다. Korean smoke는 실제 repeat fingerprint/offset과 guard 결정을 남기며, 제한된 재시도 안에 미재현이면 `not_reproduced`를 기록하되 S03 결정론적 fixture를 대체하지 않는다 |
| S08 | `filter-policy` | dev/dev-corp에서 model/provider별 policy가 다르고 request 도중 config reload와 recovery provider 전환이 발생한다 | registry가 request snapshot과 attempt actual target을 resolve한다 | 같은 config generation을 유지하면서 provider별 active set은 다시 계산하고 optional disabled는 skip하며 required unsupported 후보는 admission 전 제외한다 |
| S09 | `repeat-guard` | `dev-corp-iop-ornith`/`ornith:35b` 유형 요청 history에 같은 tool/action fingerprint와 동일/no-progress tool result가 누적되고 현재 provider 응답이 같은 tool call delta를 다시 생성한다 | action repetition threshold를 넘긴다 | IOP가 held tool call delta를 downstream으로 release하지 않고 repeated_action observation을 남기며, side-effect 가능 구간에서는 자동 repair 없이 guard terminal error 또는 안전 중단으로 무한 toolUse 루프를 끊는다 |
| S10 | `repeat-guard` | generic Chat Completions request history에서 user occurrence 0인 assistant anchor가 `content` 1회, `reasoning_content` 6회, `reasoning` 4회 누적되고 완료된 tool history는 서로 다른 action/result를 가진다 | assistant-history threshold를 넘긴다 | IOP가 Pi session 없이 role/channel provenance와 completed-progress를 판정하고, D01 정책에 따라 observe 또는 plain reasoning history sanitation/live dedupe를 수행하며 서로 다른 action을 anchor만으로 차단하지 않는다 |
| S11 | `repeat-guard` | assistant-only anchor가 request history에 반복되고 완료된 action/result가 동일 실패 또는 deterministic no-progress/churn을 보인다 | history anchor와 no-progress threshold를 함께 넘긴다 | D01에서 repair가 승인되고 tool call release 전이면 sanitized history와 원문 safe prefix로 upstream repair를 수행한다. caller 온도 지정이 없을 때만 `[0.2, 0.4, 0.6]` 후보를 순서대로 사용하고, 매 후보의 반복 구간은 제외하되 final `content`를 조용히 삭제·요약하지 않는다. release/side-effect 구간이면 observe/안전 중단한다 |
| S12 | `repeat-guard` | caller가 이전 assistant reasoning을 `messages`에 재전송하지 않고 caller-neutral conversation identity도 제공하지 않는다 | 새 request가 들어온다 | IOP는 누락된 history occurrence나 TTL lineage를 추정하지 않고 incoming content/tool history와 current stream에서 관찰 가능한 guard만 적용한다 |
| S13 | `filter-policy` | 의미와 provider capability가 같은 payload를 raw HTTP, OpenAI SDK, Pi caller가 각각 전송한다 | filter path와 decision을 평가한다 | caller/agent 제품명과 무관하게 같은 path, threshold, observation/decision을 내리고 Pi는 선택적 field evidence로만 남는다 |
| S14 | `stream-gate-adoption` | response-start/delta가 나뉘고 rolling, terminal-trigger, error-event-only filter가 함께 활성인 상태에서 추가 event가 도착한다 | codec/Edge adapter가 Core single-flight barrier를 소비한다 | ready filter는 평가되고 terminal filter는 blocking deferred, 오류 없는 provider-error는 nonblocking not-applicable이 된다. complete outcome set 전 commit하지 않고 bounded backpressure 뒤 action 하나만 실행한다 |
| S15 | `provider-error-retry` | provider response-start/status/header와 일부 body가 staged됐지만 user-visible commit 전이고 error가 `filters[]`에 매칭된다 | exact intent와 D04 1~3회 budget 및 request 전체 cap이 남아 있다 | original response-start/body를 버리고 current attempt abort 뒤 raw-canonical bounded rebuild/pool admission으로 재진입해 최종 attempt의 response-start/output만 한 번 노출한다 |
| S16 | `provider-error-retry` | status/header/role/body 중 하나가 commit돼 stream-open이고 pending이 남은 상태에서 provider error, cancel 또는 side-effect 구간이 발생한다 | error/cancel을 처리한다 | exact/schema replay하지 않고 terminal error/close로 끝낸다. text repeat의 continuation-safe recovery와 exact replay eligibility는 혼동하지 않으며 duplicate dispatch/cache를 만들지 않는다 |
| S17 | `provider-error-retry` | 같은 request/epoch에서 provider error, Tool Call validation 또는 다른 strategy의 retryable intent가 함께/연속 발생한다 | Core가 intent, attempt abort, strategy counter와 request 전체 recovery counter를 판정한다 | epoch/cycle마다 deterministic plan/dispatch 하나만 실행하고 exact는 총 3회, 모든 strategy 합계는 최초 실행 제외 기본값/절대 상한 3회 안에서만 다음 cycle을 허용한다. policy는 0~3만 허용하고 4 이상은 config validation에서 거부하며 cap 소진을 strategy 변경으로 우회하지 않고 abort 실패 시 새 provider를 시작하지 않는다 |
| S18 | `responses-codec` | `/v1/responses`의 unknown input item, response-start, text/reasoning/function-call/terminal event가 configured snapshot byte limit 안에 있다 | Responses codec과 bounded lossless Rebuilder가 normalized event/repair input을 만든다 | Chat parser를 재사용하지 않고 OpenAI JSON raw body 하나를 canonical source로 unknown/encrypted item을 보존하며 repeat continuation/path switch 뒤에도 Responses shape와 single opening/terminal을 유지한다. typed view와 rebuild 임시 output을 포함한 retained bytes가 기본값/절대 상한 16 MiB를 넘으면 no-dispatch terminal로 끝난다 |
| S19 | `schema-contract` | schema terminal gate가 활성이고 provider output이 configured hard bound를 넘는다 | Core가 overflow를 감지한다 | staged response-start/content를 release하지 않고 current attempt를 취소한 뒤 schema/guard terminal error로 끝낸다 |
| S20 | `resume-notice-builder` | D01 continuation repair가 허용됐고 content와 think/reasoning에 반복 전 모델 출력이 있다 | all-complete Arbiter가 plan 하나를 선택하고 current attempt ownership을 종료한 뒤 endpoint Rebuilder가 복구 요청을 조립한다 | 사용자 요청·message를 넣지 않고 두 channel의 원문을 구분해 고정 영어 지시문과 직접 조립한다. 출력은 요약·절단·재작성하지 않으며 문맥 한도를 넘으면 dispatch하지 않는다. 언어 판별·번역·로컬 모델·`RecoveryPlanPreparer` 호출은 없고, 실제 outbound recovery dispatch에만 budget을 소비한다. Chat Completions와 Responses가 각 endpoint shape를 보존한다 |
| S21 | `stream-gate-adoption` | Chat/Responses ingress raw body가 limit-1, limit, limit+1이거나 typed view/rebuild를 포함한 current peak retained bytes가 limit을 넘는다 | endpoint host가 body를 읽기 전에 overflow를 판정하고 SnapshotBuilder/Rebuilder가 typed view 추가 직후와 rebuild 할당 전후에 다시 계상한다 | limit-1/limit은 pre-read body gate를 통과하고 limit+1은 즉시 거부된다. 이후 initial retained overflow는 raw snapshot을 release한 HTTP 413 `invalid_request_error`, rebuild overflow는 commit state에 맞는 terminal recovery error 하나로 끝난다. 어느 overflow도 provider dispatch/recovery budget 소비/raw request 로그를 만들지 않는다 |
| S22 | `length-continuation` | managed profile의 Chat/Responses provider가 16K attempt cap에서 두 번 이상 `length` terminal을 반환하고 safe prefix가 release됐으며 rebuilt prompt/context/reserve와 (설정된 경우) caller logical cap allowance가 남아 있다 | `provider_length_gate`가 `managed_continuation` intent를 반환하고 Rebuilder가 original request와 channel별 assistant prefix를 다음 attempt로 조립한다 | 중간 `length`/`[DONE]`, response-start/role/prefix 중복 없이 같은 stream을 이어 간다. fault recovery 3회 cap은 소비하지 않으며 context 또는 caller logical cap exhaustion 때만 logical `length` terminal과 종료 marker를 한 번 보내고, complete tool boundary, cancel 또는 lossless fragment serialization 불가에서는 endpoint별 final terminal 하나로 수렴한다 |
| S23 | `observable-core-smoke` | local/dev diagnostic smoke가 deterministic Chat/Responses provider stream과 `pass`, `observe_only` violation, pre-release `blocking` violation을 반환하는 diagnostic `Filter` mock을 사용한다 | 한 명령으로 실제 codec, Core, all-complete Arbiter, Recovery Coordinator, ReleaseSink와 observation sink를 통과시킨다 | 모든 batch가 평가 전에 stage된다. pass와 observe-only는 stage/evaluate/arbitrate/release/single-terminal로 수렴하고 observe-only violation은 출력 차단 없이 관측된다. blocking은 current attempt를 abort하고 recovery를 한 번 dispatch한 뒤 새 attempt의 출력과 terminal만 한 번 전달한다. 전체 timeline의 correlation은 안정적이고 attempt 전환은 명시되며 raw prompt/output/tool args/result/auth는 구조화된 결과나 일반 로그에 남지 않는다 |
| S24 | `approved-tail-pacing` | fake monotonic clock의 200/500/1000-rune rolling pass window, short terminal tail, blocking recovery, tool fragment, cancel/sink failure와 bounded queue fixture가 Chat/Responses tunnel·normalized path에 입력된다 | filter pass로 승인된 이전 window를 scheduler가 전달하는 동안 다음 provider window가 계속 수집·평가된다 | 시간은 승인 전 release eligibility나 idle fail-open으로 사용되지 않는다. 각 approved window의 delivery duration은 수집 시간 `T`의 ±10%이고 exact rune/event/channel 순서와 UTF-8을 보존하며 threshold whole-batch burst와 `collect T + deliver T` 직렬 지연이 없다. recovery는 unapproved tail만 교체하고 structural/tool event는 원자적이며 queue drain 뒤 terminal 하나만 전달한다. cancel·sink failure는 timer/queue/provider ownership을 정리한다 |

## Evidence Map

| Scenario | Required Evidence | `agent-task` 연결 | 완료 Evidence 기대 |
|----------|-------------------|------------------|---------------------------|
| S01 | outer API·inner config 계약 문서 diff, Go request/metadata/config type tests | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `contract-doc`, pre-read raw-canonical limit/default/range/refresh 분류와 initial/rebuild error contract, `go test ./apps/edge/internal/openai -count=1` 결과 |
| S02 | mode selection unit tests, unsupported 조합 tests | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `filter-pipeline`, handler test 결과 |
| S03 | Korean multi-byte 6-paragraph repeat, 500-rune pending+look-behind, continuation cursor, duplicate opening/prefix and temperature fixtures | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `repeat-guard`, safe stream-open continuation/single `[DONE]`/byte-boundary assertion |
| S04 | tool side-effect/action repeat guard fixture | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `repeat-guard`, automatic repair block assertion |
| S05 | valid JSON schema, bounded terminal gate and eager response-start/content absence fixture | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `schema-contract`, validated-only commit assertion |
| S06 | invalid recovery, retry exhausted, lossless unknown-field RequestRebuilder fixtures | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `schema-contract`, uncommitted recovery/field preservation assertion |
| S07 | generic raw HTTP/OpenAI SDK smoke, dev `ornith:35b` Korean long-output capacity+1 stream smoke 3회, optional Pi field smoke, Edge provider log, D03 원문 기록 `on`/`off` 전환 logging tests | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `ops-evidence`, Korean smoke의 repeat fingerprint/offset 또는 `not_reproduced`, generic smoke evidence, optional caller field evidence, role preservation, `on` 원문 기록·`off` 비원문 기록·항상 제외되는 prompt/tool/auth assertion |
| S08 | qwen/gemma/ornith policy, config reload isolation, provider-switch and required capability admission fixtures | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `filter-policy`, request snapshot/attempt re-resolution/pre-dispatch 400 assertion |
| S09 | incoming request-history repeated tool/action fingerprint fixture, Ornith 반복 toolUse 로그 재현 | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `repeat-guard`, tool call delta hold/block/release assertion, repeated_action observation, no-progress threshold, observe-only 불확실성 처리, side-effect safe stop assertion |
| S10 | [2026-07-16 sanitized incident evidence](evidence/2026-07-16-pi-ornith-cross-request-history-anchor.log)에서 파생한 caller-neutral content/reasoning alias fixture, completed distinct action/result fixture | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `repeat-guard`, assistant-only provenance, incoming-history threshold, D01 decision assertion, distinct action non-block assertion |
| S11 | generic assistant anchor + completed no-progress/churn fixture, released/unreleased tool call fixture, caller 온도 지정/미지정과 후보 배열 소진 fixture | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `repeat-guard`, D01 정책, sanitized repair history, 원문 보존·요약 금지, caller 온도 보존, `[0.2, 0.4, 0.6]` 후보 소진, final content non-suppression, side-effect safe stop assertion |
| S12 | reasoning history omitted fixture, no conversation identity fixture | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `repeat-guard`, no inferred TTL/lineage와 current-request degradation assertion |
| S13 | raw HTTP/OpenAI SDK/Pi equivalent-payload table test | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `filter-policy`, caller-name-independent path/threshold/decision assertion |
| S14 | two-endpoint response-start, evaluated/deferred/not-applicable outcomes, bounded ingress and simultaneous violation tests | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `stream-gate-adoption`, blocking-deferred/event-only distinction/no eager commit assertion |
| S15 | staged response-start/body matched error, shared 1~3회 plan, request-total cap, raw-canonical equality, abort and pool readmission fixture | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `provider-error-retry`, original start hidden/final response once/total-cap assertion |
| S16 | stream-open pending error, released role/content/tool, cancel/side-effect and continuation-vs-exact matrix | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `provider-error-retry`, exact no-replay/continuation distinction/no duplicate dispatch assertion |
| S17 | simultaneous/sequential cross-strategy intents, shared exact 3회, request-total 0/1/3회와 4 이상 config rejection, abort-before-dispatch and abort-failure fixtures | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `provider-error-retry`, deterministic one plan/no overlap/no strategy bypass/absolute-cap assertion |
| S18 | Responses unknown item, response-start, split event, raw-body canonical repair, retained/rebuild overflow and path-switch fixtures | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `responses-codec`, endpoint-specific lossless shape/single opening-terminal/no-dispatch overflow assertion |
| S19 | terminal-gate hard-limit overflow/cancel/no partial release fixture | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `schema-contract`, bounded fail-closed assertion |
| S20 | all-complete/abort-before-build, content·think/reasoning channel provenance, exact fixed-English directive, user request/message exclusion, context overflow no-dispatch, Chat/Responses rebuild fixture | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `resume-notice-builder`, 고정 문구 일치/no translator·local-model·preparer call/no summary·truncation·rewrite/actual dispatch-only budget/endpoint shape assertion |
| S21 | Chat/Responses raw-body limit-1/limit/limit+1 pre-read, exact-limit body+typed-view overflow, no-full-read overflow, rebuild pre/post-allocation peak and release fixtures | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `stream-gate-adoption`, body-gate-vs-total-retained 구분/initial 413/rebuild commit-aware terminal/no-dispatch/no-budget/no-raw-log assertion |
| S22 | 16K output-cap `length` 2회 이상, original request + channel prefix/prefill rebuild, same-stream cursor/opening/prefix suppression, context/reserve 또는 caller logical cap exhaustion과 complete tool-call, lossless fragment serialization 가능/불가, cancel fixture | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `length-continuation`, fault cap과 trajectory budget 분리·no summary/truncation/new user message·single final terminal assertion |
| S23 | 한 명령 local/dev diagnostic smoke, deterministic pass/observe-only/blocking provider fixtures, Chat/Responses 실제 codec/Core/Arbiter/recovery/ReleaseSink 연결, ordered raw-free observation timeline | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `observable-core-smoke`, 모든 batch의 stage-before-evaluate/release, observe-only nonblocking, single abort/recovery, stable correlation/attempt switch, output·terminal 중복 부재와 prompt/output/tool/auth 미기록 assertion |
| S24 | fake clock 200/500/1000-rune·short-tail duration fixture, overlapping collection/delivery trace, bounded queue/backpressure, recovery/tool/cancel/sink-failure tests, dev `ornith:35b` direct OneX 대 Edge gap 비교, active spec/contract diff | `agent-task/m-openai-compatible-output-validation-filters/...` | `Roadmap Completion`에 `approved-tail-pacing`, 승인 전 no-release, window별 `T` ±10%, exact rune/event/channel order, no serial double-latency, single terminal/timer cleanup, 2ms 미만 gap 비율과 threshold burst 감소, 구현 스펙·API/config 계약 동기화 assertion |

## Cross-repo Dependencies

- 없음

## Drift Check

- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다.
- [x] Evidence Map이 code-review/complete.log에서 검증 가능하다.
- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다.
- [x] 사용자 결정과 최종 승인이 [user_review_0.log](user_review_0.log), 현재 요청의 D08과 SDD에 반영됐다.

## 사용자 리뷰 이력

- 2026-07-16: caller-neutral 경계 보강 후 D01-D03 사용자 리뷰 요청. 해결 전 SDD 잠금 유지.
- 2026-07-23: provider error filter를 `code`/`message` array로 제한하고 완료된 Tool Call Runtime validation의 exact-replay path/counter를 Core Coordinator가 흡수하도록 1차 확정했다. 이후 D04에서 최초 실행 제외 최대 3회와 기존 provider-pool 재admission 정책까지 확정했다.
- 2026-07-23: provider 출력 전체가 아닌 pending tail을 기본 500 Unicode rune의 반복 판정 증거가 쌓일 때까지 filter 앞에 보류하고, environment/model/provider policy로 값을 조정하도록 확정. 시간 경과는 미검증 출력 release 조건으로 사용하지 않는다.
- 2026-07-23: D01에서 반복 전 원문 보존·반복 구간 제외, plain reasoning 한정 sanitation/live dedupe, tool release·side-effect 구간의 자동 복구 금지, 사용자 지정 온도 제외 `[0.2, 0.4, 0.6]` 순차 복구를 확정했다. 재개 안내문의 언어 판별·번역 로컬 모델 호출은 D05로 분리해 잠금 유지한다.
- 2026-07-23: D02에서 Chat Completions와 Responses를 현재 Milestone에 함께 포함하고, 공통 filter/core는 공유하되 raw parser·repair serializer는 endpoint별 codec으로 분리하도록 확정했다.
- 2026-07-23: D03에서 assembled output/reasoning 원문 기록을 기본 `on`으로 두고 설정 `off` 뒤 요청부터 중단하도록 확정했다. 비원문 운영 정보는 남기며 raw user prompt/tool args/result/auth는 항상 제외하고 기존 로그 자동 삭제는 하지 않는다.
- 2026-07-23: D04에서 provider 오류와 Tool Call Runtime 검증의 exact replay를 최초 실행 제외 request-local 최대 3회로 공유하도록 확정했다. 재시도 가능 여부는 pending tail 존재가 아니라 Core `CommitState`로 판정하며, safe prefix release 뒤에는 tail이 남아도 replay하지 않는다. 재시도 provider 선택은 기존 provider-pool admission 정책에 맡긴다.
- 2026-07-23: 재검토에서 HTTP/SSE response-start와 opening role을 Core commit 경계에 포함하고, repeat rolling/fragment와 schema terminal hold를 분리했다. exact/schema는 `transport_uncommitted`, 반복 continuation은 safe prefix가 열린 stream에서도 cursor를 보존하는 별도 resume mode로 정리했다.
- 2026-07-23: D05의 일부를 확정했다. 재개 안내문 모델은 설정으로 분리하고 원 요청의 실패 route를 재사용하거나 translator 실패를 다른 모델 fallback으로 우회하지 않는다. 번역 호출 실패는 반복 감지와 번역 실패를 잇는 bounded sanitized 원인 사슬을 내부에 남기고, endpoint별 외부 terminal 오류 하나로 끝낸다.
- 2026-07-23: 안정성 재검토에서 request 전체 recovery는 최초 실행 제외 기본값/절대 상한 3회, ingress snapshot은 pre-read 기본값/절대 상한 16 MiB로 고정했다. OpenAI JSON raw body 하나를 exact replay canonical source로 사용하고 initial/rebuild overflow의 외부 오류 경계를 분리했다. D05 번역 호출은 filter barrier가 아니라 all-complete/attempt 종료 뒤 optional `RecoveryPlanPreparer`에서 실행하고, plan/idempotency key당 최대 1회, hard deadline, translator retry/fallback 금지, 실패 시 recovery dispatch/budget 미소비로 보강했다.
- 2026-07-23: D06에서 오류 발생 이후의 cross-request 수집·민감정보 제거·안정 지문 기반 중복 집계·소스/버전 연결·비동기 LLM 분석·수정 제안·프로젝트별 작업 문서·사용자 승인·격리 수정/테스트/독립 검토·변경 요청·병합·배포·재발 확인을 별도 범용 플랫폼으로 프로젝트화하기로 확정했다. 같은 오류는 새 작업을 만들지 않고 canonical 사건의 count만 갱신하며, 현재 Milestone은 raw-free terminal/observation event 방출까지만 소유한다.
- 2026-07-23: D05 모델 대상은 코드에 특정 모델명을 고정하지 않고, 설정이 가리키는 IOP 서빙 로컬 모델 별칭으로 부분 확정했다. 해당 별칭의 기존 model route/admission과 provider-pool 선택은 재사용하되 원 요청의 실패 route나 별칭 밖 다른 모델로 fallback하지 않는다. 실제 config field 이름은 구현 계약 결정으로 남긴다.
- 2026-07-23: D05 언어 판별은 사용자 명시 출력 언어, 마지막 정상 user-visible 출력, 마지막 사용자 요청, 설정 기본 언어 순으로 확정하고 think/reasoning 원문은 판별 입력에서 제외했다. 번역 모델은 구분된 untrusted data와 고정 복구 지시문을 받아 `target_language`/`recovery_instruction`만 반환하며, 전체 재시도 prompt 조립과 원문 보존은 runtime이 맡는다. 입력 hard bound, 언어 코드/반환 검증 한도, deadline 값과 기본 언어는 계속 잠금 항목이다.
- 2026-07-23: D05 번역 호출 실패 시 외부 terminal 오류 안내문은 설정된 오류 안내 기본 언어를 사용하고 설정이 없으면 영어(`en`)를 쓰도록 확정했다. 실패한 번역 모델은 오류 안내문 번역을 위해 다시 호출하지 않는다.
- 2026-07-23: D05 번역 호출의 hard deadline은 provider-pool 대기, 모델 생성, 구조화된 반환 검증을 모두 합산해 20초로 확정했다. 초과 시 translator 재호출이나 다른 모델 fallback 없이 번역 실패 terminal 경로로 종료한다.
- 2026-07-23: D05 번역 입력은 마지막 사용자 요청 전체 텍스트를 기존 16 MiB ingress snapshot 한도 안에서 절단·요약 없이 전달하고, user-visible 출력은 기존 500 Unicode rune 표본을 사용하도록 확정했다. 번역 모델 context 한도를 넘으면 입력을 줄이지 않고 `recovery_notice_translation_input_too_large`로 실패 처리한다.
- 2026-07-23: D05 번역 입력을 다시 좁혀 사용자 요청을 제외하는 것으로 앞선 결정을 대체했다. 번역 모델은 user-visible 출력 500 Unicode rune, 설정 기본 언어와 고정 지시문만 받고 언어 판별도 `user-visible 출력 → 설정 기본 언어`만 사용한다. 원래 사용자 요청은 runtime 재작업 prompt 조립에서만 유지한다.
- 2026-07-23: D01/D05 복구 입력을 모델 응답 content로만 제한하는 것으로 다시 확정했다. 사용자 요청·message는 번역 호출뿐 아니라 실제 재작업 요청에서도 제외하며 think/reasoning도 넣지 않는다. 번역 표본은 모델 응답 content의 마지막 500 Unicode rune이고, 재작업 요청은 반복 전 모델 응답 content 원문과 고정 복구 지시문만 사용한다.
- 2026-07-23: D01/D05 모델 출력 범위를 content와 think/reasoning 두 channel로 다시 확장했다. 번역 입력은 channel별 마지막 500 Unicode rune을 사용하고 `content → think/reasoning → 설정 기본 언어` 순으로 언어를 정한다. 재작업 요청도 반복 전 두 channel 원문과 복구 지시문만 사용하며 사용자 요청·message는 계속 제외한다.
- 2026-07-23: D05의 앞선 번역 관련 결정을 모두 대체했다. 언어 판별·번역·로컬 모델 호출·모델 별칭·20초 제한시간·구조화 반환·번역 실패 오류는 구현하지 않고, 반복 복구에는 고정 영어 지시문을 직접 사용한다. 재작업 요청은 반복 전 content와 think/reasoning 원문을 channel별로 구분해 넣고 사용자 요청·message는 제외한다.
- 2026-07-23: 사용자의 최종 승인을 반영해 SDD 상태를 `[승인됨]`, SDD 잠금을 `해제`로 전환하고 사용자 리뷰를 [user_review_0.log](user_review_0.log)로 보존했다.
- 2026-07-24: 사용자가 provider의 큰 `max_tokens`가 local scheduling·동시성에 주는 부작용을 작은 attempt cap과 IOP managed continuation으로 분리하도록 확정했다. `length` 중간 terminal은 숨기고 원본 요청과 channel별 assistant prefix를 보존해 context-window/reserve가 허용하는 논리 trajectory를 같은 stream에 이어 간다. 이는 fault recovery 3회 cap과 별도이며 요약·문장 경계 절단·새 user message 방식은 사용하지 않는다.
- 2026-07-25: 재검증에서 rebuilt prompt가 assistant prefix를 이미 포함하므로 누적 output 이중 계상을 제거했다. provider attempt cap은 내부 운영 단위로만 쓰고 caller 명시 output cap은 논리 요청 전체에 한 번 적용한다. context 또는 caller logical cap 소진은 중간 provider terminal이 아닌 endpoint-native logical `length` terminal 한 번으로 표현하며, 미완성 tool fragment는 lossless serializer가 있는 경우에만 내부 continuation prefix로 사용한다.
- 2026-07-28: 사용자가 실제 의미 필터 구현 전에 pipeline 자체를 관측·검증할 deterministic diagnostic mock smoke를 선행 조건으로 확정했다. pass, observe-only violation, blocking violation 뒤 단일 recovery가 실제 codec/Core/Arbiter/ReleaseSink와 raw-free observation 경로를 통과해야 하며 production 기본 등록과 caller 활성화는 금지한다.
- 2026-08-02: 사용자가 threshold whole-batch burst를 완화하기 위해 window 수집 시간만큼 승인된 tail을 균일하게 전달하는 정책을 확정했다. 시간은 filter 승인 전 release 조건이 아니며 `pending -> approved -> delivered`를 분리하고, 이전 approved delivery와 다음 provider collection을 겹쳐 직렬 `collect T + deliver T` 지연을 만들지 않는다.

## 작업 컨텍스트

- 표준선: 첫 구현 단위는 `observable-core-smoke`다. diagnostic mock은 결정론적 filter decision만 제공하고 실제 host codec/Core/Arbiter/recovery/ReleaseSink/observation sink를 대체하지 않는다. pass·observe-only·blocking recovery timeline, stable correlation/attempt switch, 출력·terminal 단일성과 raw-free invariant를 한 명령의 구조화된 smoke 결과로 확인하기 전에는 `repeat-guard`, `schema-contract`, `provider-error-retry` 구현으로 넘어가지 않는다.
- 표준선: OpenAI-compatible provider 출력 검증 host adapter는 Edge OpenAI handler/stream bridge에 두고 transport-agnostic Core는 `packages/go/streamgate`에 둔다. normalized 실행 path를 tunnel path로 강제 전환하지 않지만 두 path의 provider output은 endpoint codec 뒤 같은 Core event contract를 사용할 수 있다.
- 표준선: `provider_length_gate`는 provider의 output-cap terminal만 caller-neutral하게 판정한다. managed profile은 작은 attempt cap으로 dispatch하고, original request와 content/think/reasoning assistant prefix를 endpoint별 shape로 rebuild한다. prefill capability가 있으면 우선 사용하며 없으면 고정 internal continuation directive만 더한다. Core의 `ManagedTrajectoryBudget`은 rebuilt prompt token과 reserve를 뺀 실제 context-window 여유 및 caller가 명시한 논리 output cap을 장문 상한으로 사용하며, prefix를 누적 output으로 이중 계상하지 않고 fault recovery 3회 cap과 분리한다. provider attempt cap은 내부 운영 단위다.
- 표준선: Chat/Responses codec은 response-start 포함 raw parser와 lossless `RequestRebuilder`를, Edge는 `AttemptDispatcher`/`AttemptController`/`ReleaseSink`를 제공한다. Core가 active filter single-flight fan-out/all-complete, deterministic action, commit, budget/abort/rebuild 호출/single re-admission을 담당한다.
- 표준선: provider 출력은 `host dispatch → response-start/event codec → Core hold → active filters parallel evaluation → all-complete Arbiter → staged release 또는 RecoveryPlan → current attempt abort → rebuild → single re-admission` 순서다. repeat는 500-rune rolling, schema는 hard-bound terminal gate이므로 기본 streaming 경로만 전체 응답을 모으지 않는다.
- 표준선: rolling pass의 release는 synchronous whole-batch sink loop가 아니라 bounded approved queue와 비동기 pacing scheduler를 사용한다. 첫 eligible delta부터 threshold/terminal까지의 monotonic 수집 시간 `T`와 approved rune 수를 immutable item에 고정하고 frame tick 누적 quota로 전달한다. 이전 approved item 전달 중 다음 window를 계속 수집·평가하며 queue full에서만 backpressure를 건다. 시간은 승인 전 eligibility가 아니고 recovery는 unapproved tail만 교체하며 structural/tool event 순서, drain 뒤 단일 terminal, cancel/sink-failure cleanup을 보존한다.
- 표준선: request 시작 시 Registry/config generation과 required capability를 고정한다. actual provider/path별 active set은 같은 snapshot에서 attempt마다 다시 resolve하고 optional filter만 skip할 수 있다. blocking/observe-only failure는 명시적 outcome이며 filter는 concurrency, request mutation, retry loop/counter, submit을 소유하지 않는다.
- 표준선: action 반복 guard는 단일 응답 텍스트 반복과 별도 축이다. Edge는 provider stream의 tool call delta와 request/message history를 이용해 `tool name + normalized args` fingerprint 반복을 감시하고, Pi agent 쪽 local tool invocation guard는 보조 방어로 둘 수 있지만 IOP 필터의 필수 의존성으로 두지 않는다.
- 표준선: action 반복 guard는 false positive를 줄이기 위해 observe-only evidence 수집을 먼저 허용하고, consecutive 동일 fingerprint와 동일/no-progress 결과가 threshold를 넘을 때 guard mode에서 중단한다. read-only 반복 action은 model/provider/tool별 threshold 또는 allow policy로 조정한다.
- 표준선: provider 오류 filter는 `filters[] = [{ code, message }]` 매칭과 Core `CommitState=transport_uncommitted`일 때만 exact intent를 반환한다. staged response-start/body는 commit이 아니며 Core가 기존 exact 실행을 흡수해 최초 실행 제외 exact 최대 3회, 모든 strategy를 합산한 기본값/절대 상한 3회 request 전체 cap, pre-read 기본값/절대 상한 16 MiB raw-canonical ingress snapshot, abort-before-dispatch와 pool re-admission을 관리한다. stream-open 뒤에는 pending이 남아도 exact replay하지 않으며 retry provider는 기존 pool 정책이 선택한다.
- 표준선: assistant history anchor는 single-stream content 반복과 identical action 반복 사이의 별도 축이다. provider/caller가 바뀌었다고 가정하지 않고 현재 incoming history의 role/channel provenance만 source of truth로 사용한다. D01은 plain reasoning만 sanitize/live dedupe하되, continuation 복구 입력은 반복 전 모델 응답 content와 think/reasoning 원문을 channel별로 구분해 고정 영어 지시문 `The previous model output was stopped after a repetition loop was detected. Continue from the provided content and reasoning output without repeating already generated text.`와 사용하고 원래 사용자 요청·message는 넣지 않는다. 반복 구간만 제외하며 두 channel 원문을 의미 요약·임의 절단·재작성하지 않는다. caller가 온도를 지정하지 않은 경우에만 `[0.2, 0.4, 0.6]`을 순서대로 한 번씩 적용하며, 배열 소진·문맥 한도 초과·tool release·side-effect 구간이면 자동 복구하지 않는다. 언어 판별·번역·별도 모델 호출이나 `RecoveryPlanPreparer`는 사용하지 않고 current attempt ownership 종료 뒤 endpoint별 Rebuilder가 복구 요청을 직접 조립한다.
- 표준선: generic raw HTTP/OpenAI SDK fixture가 완료 기준이며 Pi는 2026-07-16 incident evidence와 선택적 field smoke다. 원본 host session과 dev Edge 로그는 장기 문서에 복제하지 않고 SDD 옆 sanitized evidence에는 비민감 anchor 예시, 시간/role/channel/count/hash, provider 전환, action/result 집계, adapter payload 재구성 경계만 남긴다.
- 표준선: 한국어 장문 출력은 결정론적 single-stream 반복 fixture로 고정할 회귀 축이다. 2026-07-22~23 `ornith:35b` live smoke에서는 반복을 확인하지 못했고, downstream release 전 llama-server HTTP 500 `Failed to parse input at pos`만 관측됐다. S03 fixture는 긴 한국어 문단 6개를 UTF-8 multi-byte 경계로 분할한 뒤 다시 반복해 rolling inspector의 byte-safe 판정을 고정한다. S07 dev smoke는 `stream=true`, model group 총 capacity+1, 최소 3회로 실행하고 raw SSE/출력은 단기 ignored `agent-test/runs/**`에만 둔다. live 재현은 확률적이므로 `not_reproduced`가 S03 fixture의 통과를 대체하지 않으며, tracked SDD/로드맵에는 raw prompt 또는 전체 출력 원문을 복제하지 않는다.
- 표준선: 별도 오류 수정 플랫폼의 내부 공통물은 특정 프로젝트의 `Milestone` 형식이 아니라 중립 수정 제안으로 두고, 프로젝트 adapter가 IOP 마일스톤·이슈·티켓 등으로 변환한다. IOP 요청 응답은 비동기 분석 완료를 기다리지 않으며, 플랫폼 자체 오류는 같은 분석 루프에 재투입하지 않는 격리·호출 제한·차단기 경계를 별도 프로젝트 SDD에서 확정한다.
- 후속 SDD: 없음
