Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
9.5 KiB
9.5 KiB
id, status
| id | status |
|---|---|
| dcy-0008 | confirmed |
Reconcile-warned strategy intent rejection alerts
Problem
활성 reconciliation warning이 있는 slot에 외부 strategy가 신규 buy 또는 sell intent를 제출하면 IntentService는 reconcile_warned 사유로 안전하게 거부하지만, 현재는 RPC 응답과 intent_log 외에 운영자에게 즉시 전달되는 신호가 없다. Strategy가 마지막 slots RPC 이후 발생한 warning을 알지 못한 채 intent를 제출하는 race에서도 거부 자체는 정상 동작하지만, 운영자는 반복되는 거부와 전략 정체의 원인을 별도 조사 전까지 인지하기 어렵다.
기대 상태는 durable intent idempotency와 기존 reconciliation 차단 권한을 유지하면서, 새 intent_id가 최초로 reconcile_warned 최종 거부된 시점에만 allowlist 기반 trex.alerts 경고를 best-effort로 한 건 발행하는 것이다. 동일 intent_id의 retry는 저장된 결정을 그대로 반환하되 추가 경고를 만들지 않아야 하며, 알림 실패가 intent 거부·응답·저장 또는 다른 business path를 바꾸어서는 안 된다.
Evidence
- GitHub issue #74는 실운영에서 warning 활성 뒤 strategy intent가 거부됐지만 즉시 알림이 없어 원인 파악이 지연된 사례를 기록하고, 최초
reconcile_warned거부 알림·중복 억제·민감정보 제외·non-blocking 동작을 요구한다. 확인 시 issue는 open이고 후속 comment나 연결된 PR은 없었다. - 현재
trex_core/application/intents.py의submit_buy()와submit_sell()은IntentStore.try_insert()를 dedup gate로 호출한 뒤 admission 결과를finalize()하고 반환한다. Duplicateintent_id는_dedup()에서 기존 결정을 반환하므로 새 admission을 실행하지 않는다. - 같은 파일의 buy·sell warning gate는
warn_store.is_warned(slot_id)가 참이면IntentDecision(status="rejected", reason="reconcile_warned")을 반환한다.IntentServiceconstructor에는alarm_emitter의존성이 없고, 이 최종 거부를trex.alerts에 발행하는 호출도 없다. trex_core/runtime/daemon.py는 하나의AlarmEmitter를 생성해 ledger, kill-switch, scheduler와 execution service에 주입하지만IntentService에는 주입하지 않는다. 따라서 production intent path에는 재사용 가능한 publisher가 존재하지만 wiring이 빠져 있다.trex_core/application/alarm_emitter.py의publish_alarm()은severity로critical | warn | info를 사용하고 publish exception을 삼켜 호출자의 business path를 중단하지 않는 best-effort 계약이다.docs/interface-contract.md도 warning wire literal을warn으로 정의한다.trex_core/adapters/persistence/postgresql.py의PostgreSQLIntentStore는intent_idunique insert를 durable dedup gate로 사용하고 finalstatus,slot_id,reason을 저장한다. 별도 notification state 없이도 “최초 처리된 intent만 발행 후보”라는 경계는 이 기존 gate로 정할 수 있다.tests/application/test_reconcile_warning_blocking.py와tests/application/test_slots_handler_quantity_mismatch.py는 buy/sellreconcile_warned거부와 last-RPC race를 검증하지만 alarm 발행을 단언하지 않는다.tests/application/test_messaging_intent_consumer.py는 duplicateintent_id가 저장된 동일 결정을 반환하는 idempotency를 검증한다.- 현재
mainrevision2e314232e3073381b6fc9ee268ae8e12fbcc8749에서 관련 baseline인tests/application/test_reconcile_warning_blocking.py,tests/application/test_slots_handler_quantity_mismatch.py,tests/application/test_messaging_intent_consumer.py,tests/application/test_alarm_emitter.py를 실행한 결과84 passed였다. - Git history에서
c193388은 external strategy buy/sell에 warning gate를 의도적으로 추가하면서 safety-sell·kill-switch·watchdog가IntentService를 우회하도록 정했다. 이번 Line은 그 차단 정책이나 내부 안전 경로를 변경하지 않고, 이미 확정된 외부 intent 거부의 운영 가시성만 추가한다.
Scope
- AC admission은 새
AC-0025create로 분류한다. 가장 가까운AC-0023은 C-case strategy RPC 제외·intent blocking·warning clear 권한을 소유하고,AC-0024는 B/C mismatch 자체의 detected/changed/resolved lifecycle을 소유한다. 이번 동작은 “외부 strategy intent가 실제로 차단된 최초 사건”이라는 별도 운영 신호이며 독립 PASS/FAIL과 장기 active 가치가 있으므로 기존 AC의 update 또는 satisfy로 흡수하지 않는다.update,retire,satisfy,release evidence대상은 없다. IntentService의 buy·sell 처리에서 새intent_id의 최종 decision이status="rejected"및reason="reconcile_warned"로 확정되고IntentStore.finalize()가 성공한 뒤에만AlarmEmitter.publish_alarm()을 한 번 호출한다.- Alarm은 기존 wire 계약에 맞춰
severity="warn", codestrategy_intent_rejected를 사용한다. Fields는 최소 allowlist인instance_key,intent_id,intent_type,reason,broker_submitted=false를 포함하고, 요청 또는 canonical slot에서 안전하게 확인 가능한 경우에만slot_id,portfolio_id,symbol, normalizedstrategy_id를 추가한다. - Buy의 canonical
slot_id입력 또는 legacyportfolio_idmapping과 sell의slot_id입력 모두에서 가능한 slot context를 수집하되, context 조회 실패나 optional field 부재는 원래reconcile_warneddecision을 바꾸거나 알림을 위해 새로운 mutation을 유발하지 않는다. - 동일
intent_id의 duplicate/retry는 기존 durableintent_log결정을 반환하고 alarm을 재발행하지 않는다. 최초 처리 중 관찰되는processing/duplicate_in_flight응답도 별도 alarm 대상이 아니다. - Alarm publish 성공 여부와 무관하게 원래 거부 decision과 RPC 응답은 유지된다. Publisher 부재와 transport failure는 non-blocking이며 별도 durable outbox, notification-state row 또는 retry loop를 만들지 않는다.
- Alarm title·fields·rendered log에는 credential, token, account number/hash, DSN, raw strategy payload, raw broker/provider payload와 raw exception body를 포함하지 않는다.
- Production daemon은 이미 생성하는 동일
AlarmEmitter를IntentService에 주입한다. Optional dependency를 사용하는 직접 application test와 non-daemon construction은 기존 동작을 유지한다. - Verification은 buy와 sell의 최초 경고 각 한 건, 같은
intent_idretry의 추가 경고 0건, 다른intent_id의 독립 경고, admitted 및 다른 rejection reason의 무발행, publish failure의 non-blocking, payload allowlist·broker_submitted=false, production wiring과 기존 warning blocking/idempotency regression을 포함한다.
Out of Scope
AC-0023의 B/C warning 판정, slots RPC 제외, buy/sell 차단, fail-closed 및 authoritative clear semantics 변경.AC-0024의 mismatch detected/changed/resolved alarm과reconcile_notification_state변경 또는 통합.- Strategy retry·backoff·pause·circuit-breaker 정책, strategy 또는 TVR wire schema 변경, 새로운 broker order submit/cancel 동작.
- Safety-sell, kill-switch, watchdog 등
IntentService를 우회하는 내부 안전 경로의 차단 또는 알림. - 다른 rejection reason에 대한 범용 intent rejection alarm, 알림 집계·rate limit·severity escalation.
- Alarm delivery를 위한 durable outbox, 별도 notification table, 재시작 후 재발행 또는 publish retry 보장.
finalize()성공과 alarm publish 사이 process crash로 인한 알림 유실 가능성은 이 Line에서 해소하지 않는다. trex-notifier의 message formatting·routing·delivery channel 변경과 운영 배포 자체.
Risks and Unknowns
Risks
IntentStore.finalize()뒤 best-effort publish를 수행하므로 process가 그 사이 종료되거나 transport가 실패하면 durable decision은 남지만 alarm은 유실된다. Issue가 outbox/retry를 제외했으므로 이 Line은 business path 독립성을 우선하며, delivery 보장이 필요해지면 별도 Requirement로 다룬다.- Buy는 canonical
slot_id와 legacyportfolio_id두 입력을 허용하고 sell은sell_strategy_id를 사용한다. Context 정규화가 admission logic을 복제하면 drift 위험이 있으므로, 알림은 decision에 영향을 주지 않는 helper와 allowlist output으로 제한하고 양쪽 입력 형태를 테스트해야 한다. - 기존
AlarmEmitter는 여러 subsystem이 공유한다. Intent-specific 실패 처리나 logging 변경이 공용 emitter 계약을 확장하지 않도록 production wiring과 publisher failure test를 분리해야 한다. - Rejection payload에 raw request 또는
extradict를 복사하면 민감정보가 유출될 수 있다. 허용된 scalar field만 명시적으로 구성하고 forbidden key/value sentinel을 rendered payload와 log에서 검사해야 한다.
Assumptions
- Issue #74의 “경고(WARNING)”는 현재 interface contract의 wire literal
severity="warn"을 의미한다. - “최초”의 canonical identity는 기존
IntentStore.try_insert()가 소유하는intent_id이며, process-local set이나 새로운 persistence를 추가하지 않는다. strategy_intent_rejected는 issue가 제시한 이 기능의 canonical alarm code로 사용하며, notifier-side inventory 등록이 별도로 필요하더라도 core의 event contract를 변경하지 않는다.