From 4cafc91323fc36c378d67eede1ad724cc3cafd2d Mon Sep 17 00:00:00 2001 From: toki Date: Sun, 2 Aug 2026 22:30:25 +0900 Subject: [PATCH] =?UTF-8?q?fix(transport):=20=ED=84=B0=EB=84=90=20?= =?UTF-8?q?=EC=A7=80=EC=97=B0=EC=97=90=20=EB=A7=9E=EC=B6=B0=20heartbeat=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=84=EB=A5=BC=20=EB=B3=B4=EA=B0=95=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 장시간 provider prefill과 stream backpressure가 정상 노드를 끊지 않도록 liveness window를 확장하고 Chronos 분리 완료 문서와 잔여 artifact를 정리한다. --- agent-roadmap/ROADMAP.md | 8 +- .../phase/automation-runtime-bridge/PHASE.md | 56 ++++---- agent-spec/index.md | 4 +- agent-spec/runtime/edge-node-execution.md | 94 ++++++++++--- .../WORK_LOG.md | 7 - .../components/console-shell/index.md | 4 +- apps/edge/README.md | 1 + .../edge/internal/transport/heartbeat_test.go | 10 +- apps/edge/internal/transport/server.go | 7 +- apps/node/README.md | 1 + apps/node/internal/transport/client.go | 8 +- .../node/internal/transport/heartbeat_test.go | 10 +- ...chronos-transfer-state-v1.contract-test.jq | 133 ------------------ 13 files changed, 137 insertions(+), 206 deletions(-) rename agent-roadmap/{ => archive}/phase/automation-runtime-bridge/PHASE.md (68%) delete mode 100644 agent-task/m-iop-agent-chronos-extraction-decoupling/WORK_LOG.md delete mode 100644 scripts/fixtures/iop-agent-chronos-transfer-state-v1.contract-test.jq diff --git a/agent-roadmap/ROADMAP.md b/agent-roadmap/ROADMAP.md index e32fda83..d25aa137 100644 --- a/agent-roadmap/ROADMAP.md +++ b/agent-roadmap/ROADMAP.md @@ -25,7 +25,7 @@ Anthropic-compatible Messages API는 Edge가 직접 제공해 Claude Code를 포 IOP의 외부 추론 호출 계약은 OpenAI-compatible API 방식을 기본 표면으로 채택하고, model/provider route, 요청 상관관계, usage, 취소·상태처럼 IOP가 소유하는 의미만 제한된 `metadata` 또는 IOP native endpoint의 명시 필드로 전달한다. IOP native protocol은 proto-socket을 기본으로 하며, HTTP는 OpenAI-compatible/A2A/health/bootstrap처럼 필요한 경계에서만 사용한다. A2A는 provider-backed 요청을 수용하는 호환 표면으로 유지하며, workflow 의미를 도입하지 않는다. -현재 제1 active delivery는 완료된 `iop-agent` 자산을 Chronos 수용 bundle로 전달한 뒤 IOP의 workspace agent·CLI agent session·terminal 및 Chronos 연결 surface를 제거하고, IOP Node에는 추론 provider 운영에 필요한 경계만 남기는 것이다. 이 분리 작업의 진행 내용은 별도 작업 checkout의 활성 Milestone을 원본으로 동기화한다. +`iop-agent` 자산의 Chronos 수용 bundle 전달과 IOP의 workspace agent·CLI agent session·terminal·Chronos 연결 surface 제거는 완료됐다. 현재 active delivery는 [IOP 실행 프리셋과 Hot Path](phase/knowledge-tool-optimization-extension/milestones/iop-hot-path-one-shot-execution.md)이며, IOP Node에는 추론 provider 운영 경계만 유지한다. IOP 내부 라우팅 축은 외부 model을 전체 execution preset에 매핑하고 `direct/light` Hot Path와 논리 `request_id` coordinator를 구축한 뒤, `heavy` Plan/Review, cloud-first preset mode 라우팅과 routing evidence 기반 local selector 전환으로 확장한다. 모델 선택, 요청 난이도에 따른 execution mode, 로컬/클라우드 라우팅, 외부 model별 execution preset, token/속도/품질 최적화, 모델 호출 로그와 품질 평가는 IOP 책임으로 둔다. 외부 model 선택이 preset을 고정하고 Edge가 model advisory와 deterministic hard gate를 결합해 allowed mode와 stage binding을 확정하며, Node는 확정된 provider stage를 실행한다. Control Plane은 principal과 IOP token, 사용자별 provider credential slot의 원장을 소유하고 Edge는 principal별 route와 제한된 credential lease를 실행에 사용한다. @@ -75,9 +75,9 @@ Phase는 실행 순서가 아니라 도메인/책임 영역의 구조적 지도 - 경로: [PHASE.md](phase/update-plane-self-update-foundation/PHASE.md) - 요약: frontend와 Control Plane만 재배포해도 Edge/Node가 안정 업데이트 프로토콜, 로컬 상태 캐시, host-local manager를 통해 스스로 버전 수렴하는 기반을 정리한다. -- [진행중] Automation Runtime과 Bridge 확장 - - 경로: [PHASE.md](phase/automation-runtime-bridge/PHASE.md) - - 요약: 완료된 `iop-agent`의 source·contract·test·config·state·build·document 자산을 repository-neutral Chronos acceptance bundle로 전부 전달하고 IOP의 관련 surface와 의존성을 제거하는 작업을 최우선 선행 Milestone으로 수행한다. 이 완료 evidence가 Chronos Roadmap의 외부 잠금을 해제한 뒤에만 실제 repository import와 Chronos Server/Node의 작업 루프·agent·terminal 제어를 시작하며, IOP Node에는 추론 provider 운영 경계만 남기고 Chronos 연결점을 두지 않는다. +- [완료] Automation Runtime과 Bridge 확장 + - 경로: [PHASE.md](archive/phase/automation-runtime-bridge/PHASE.md) + - 요약: `iop-agent`의 source·contract·test·config·state·build·document 자산을 repository-neutral Chronos acceptance bundle로 전달하고 IOP의 관련 surface와 의존성을 제거했다. 완료 evidence로 Chronos Roadmap의 외부 잠금을 해제했으며, 이후 Chronos Server/Node의 작업 루프·agent·terminal 제어는 Chronos가 소유한다. IOP Node에는 추론 provider 운영 경계만 남기고 Chronos 연결점을 두지 않는다. - [계획] 지식과 도구 최적화 확장 - 경로: [PHASE.md](phase/knowledge-tool-optimization-extension/PHASE.md) diff --git a/agent-roadmap/phase/automation-runtime-bridge/PHASE.md b/agent-roadmap/archive/phase/automation-runtime-bridge/PHASE.md similarity index 68% rename from agent-roadmap/phase/automation-runtime-bridge/PHASE.md rename to agent-roadmap/archive/phase/automation-runtime-bridge/PHASE.md index 3a24a0a4..8b0a725f 100644 --- a/agent-roadmap/phase/automation-runtime-bridge/PHASE.md +++ b/agent-roadmap/archive/phase/automation-runtime-bridge/PHASE.md @@ -2,13 +2,13 @@ ## 상태 -[진행중] +[완료] ## 목표 과거 IOP 안에서 공통화한 CLI Agent/Automation 자산을 독립 Chronos 플랫폼으로 이전하고, IOP를 추론 provider 운영 책임으로 되돌린다. 완료된 CLI 실행, workspace agent, Agent Task selector와 standalone `iop-agent`는 이전 기준선일 뿐 IOP의 장기 제품 경계가 아니다. -[[separation-01] IOP Agent Runtime의 Chronos 전체 이전과 IOP 의존성 제거](../../archive/phase/automation-runtime-bridge/milestones/iop-agent-chronos-extraction-decoupling.md)가 모든 관련 자산을 repository-neutral Chronos acceptance bundle로 전달하고 IOP의 agent·workspace·terminal·host/client lifecycle surface와 Chronos 연결점을 제거했다. 이후 실제 repository import, 최종 layout 결정, 외부 접근 가능한 Chronos Server, 독립 Chronos Node, 작업 루프·agent·terminal·원격 호스트 제어는 Chronos가 소유한다. +[[separation-01] IOP Agent Runtime의 Chronos 전체 이전과 IOP 의존성 제거](milestones/iop-agent-chronos-extraction-decoupling.md)가 모든 관련 자산을 repository-neutral Chronos acceptance bundle로 전달하고 IOP의 agent·workspace·terminal·host/client lifecycle surface와 Chronos 연결점을 제거했다. 이후 실제 repository import, 최종 layout 결정, 외부 접근 가능한 Chronos Server, 독립 Chronos Node, 작업 루프·agent·terminal·원격 호스트 제어는 Chronos가 소유한다. IOP Control Plane·Edge·IOP Node에는 model/provider/device 서빙에 필요한 route·실행·취소·상태·usage·lifecycle만 남긴다. Chronos는 필요할 때 IOP의 외부 추론 API를 일반 client로 소비하며 IOP Node에 연결하거나 제어하지 않는다. ## Milestone 흐름 @@ -16,111 +16,111 @@ IOP Control Plane·Edge·IOP Node에는 model/provider/device 서빙에 필요 완료되었거나 `[폐기]` 상태인 Milestone은 archive 경로를 가리키고, 검토중, 진행중, 계획, 스케치 또는 보류 Milestone은 이 Phase 하위 `milestones/` 경로를 가리킨다. 이 흐름은 해당 Phase 안의 상태 정리이며, Phase를 가로지르는 실행 순서는 아니다. Milestone은 완료, 폐기, 검토중, 진행중, 계획, 스케치 또는 보류 상태 그룹으로 정리한다. -Phase를 가로지르는 실제 다음 작업 선택은 [전역 마일스톤 실행 순서](../../priority-queue.md)를 우선한다. +Phase를 가로지르는 실제 다음 작업 선택은 [전역 마일스톤 실행 순서](../../../priority-queue.md)를 우선한다. - [완료] CLI Automation Runtime 안정화 - - 경로: [cli-automation-runtime-stabilization](../../archive/phase/automation-runtime-bridge/milestones/cli-automation-runtime-stabilization.md) + - 경로: [cli-automation-runtime-stabilization](milestones/cli-automation-runtime-stabilization.md) - 요약: one-shot, persistent terminal, opencode SSE, codex exec 계열 CLI 실행 모드와 운영 command 응답을 같은 adapter execution 모델 안에서 안정화했다. - [완료] Agent Bootstrap과 Domain Agent 등록 - - 경로: [agent-bootstrap-domain-agent-enrollment](../../archive/phase/automation-runtime-bridge/milestones/agent-bootstrap-domain-agent-enrollment.md) + - 경로: [agent-bootstrap-domain-agent-enrollment](milestones/agent-bootstrap-domain-agent-enrollment.md) - 요약: Edge 직접 연결 specialized domain agent 등록을 위한 MVP enrollment 계약과 후속 구현 Milestone 경계를 확정했다. - [완료] Domain Agent Registry 구현 - - 경로: [domain-agent-registry-implementation](../../archive/phase/automation-runtime-bridge/milestones/domain-agent-registry-implementation.md) + - 경로: [domain-agent-registry-implementation](milestones/domain-agent-registry-implementation.md) - 요약: Edge-side `generic-node`/domain-agent 구분을 config/store, registry/transport, service snapshot에 반영하고 검증을 완료했다. - [완료] Domain Agent Bootstrap Command 발급 - - 경로: [domain-agent-bootstrap-command-issue](../../archive/phase/automation-runtime-bridge/milestones/domain-agent-bootstrap-command-issue.md) + - 경로: [domain-agent-bootstrap-command-issue](milestones/domain-agent-bootstrap-command-issue.md) - 요약: `iop-edge agent register`가 domain agent record와 Linux bootstrap script 인자를 포함한 one-line command를 발급하고 config roundtrip 검증을 완료했다. - [완료] IOP 전용 Domain Agent 경로 제거 - - 경로: [domain-agent-specific-iop-removal](../../archive/phase/automation-runtime-bridge/milestones/domain-agent-specific-iop-removal.md) + - 경로: [domain-agent-specific-iop-removal](milestones/domain-agent-specific-iop-removal.md) - 요약: 독립 Control Plane 전환에 맞춰 IOP 활성 코드와 문서에서 전용 agent kind, bootstrap, smoke, 운영 문구를 제거했다. - [완료] 전역 설계 부채 리팩토링 기반 - - 경로: [architecture-refactor-foundation](../../archive/phase/automation-runtime-bridge/milestones/architecture-refactor-foundation.md) + - 경로: [architecture-refactor-foundation](milestones/architecture-refactor-foundation.md) - 요약: 원격 터미널 브리지와 이후 운영 확장 전에 Edge, Node, Control Plane, Client, proto/config 경계의 설계 부채를 정리했고, 잔여 리스크는 후속 선행 안정화 마일스톤으로 넘겼다. - [완료] OpenAI Responses Input Surface - - 경로: [openai-responses-input-surface](../../archive/phase/automation-runtime-bridge/milestones/openai-responses-input-surface.md) + - 경로: [openai-responses-input-surface](milestones/openai-responses-input-surface.md) - 요약: Edge OpenAI-compatible 입력 표면에 non-streaming `POST /v1/responses`, metadata 전달 계약, response subset, `iop-edge smoke openai` Responses 검증을 추가하고 실제 Edge/Node/fake Ollama smoke evidence를 확보했다. - [폐기] Specialized Agent proto-socket 연결 기반 - - 경로: [specialized-agent-proto-socket-foundation](../../archive/phase/automation-runtime-bridge/milestones/specialized-agent-proto-socket-foundation.md) + - 경로: [specialized-agent-proto-socket-foundation](milestones/specialized-agent-proto-socket-foundation.md) - 요약: domain agent가 독립형 실행 방식으로 전환될 예정이므로 Edge 직접 proto-socket specialized peer 기준의 연결 기반 정리는 폐기한다. - [폐기] Domain Agent Registration Online Smoke - - 경로: [domain-agent-registration-online-smoke](../../archive/phase/automation-runtime-bridge/milestones/domain-agent-registration-online-smoke.md) + - 경로: [domain-agent-registration-online-smoke](milestones/domain-agent-registration-online-smoke.md) - 요약: 독립형 실행 방식으로 전환될 예정이므로 Edge 직접 registration online smoke는 폐기한다. - [폐기] Domain Agent Registration Unlock Handoff - - 경로: [domain-agent-registration-unlock-handoff](../../archive/phase/automation-runtime-bridge/milestones/domain-agent-registration-unlock-handoff.md) + - 경로: [domain-agent-registration-unlock-handoff](milestones/domain-agent-registration-unlock-handoff.md) - 요약: 독립형 실행 전환으로 Edge 직접 등록 흐름 잠금 해제 handoff가 필요 없어져 폐기한다. - [폐기] Domain Agent Message Boundary - - 경로: [domain-agent-message-boundary](../../archive/phase/automation-runtime-bridge/milestones/domain-agent-message-boundary.md) + - 경로: [domain-agent-message-boundary](milestones/domain-agent-message-boundary.md) - 요약: 독립형 실행 전환으로 Edge 직접 domain payload boundary 정리가 현재 범위에서 필요 없어져 폐기한다. - [폐기] 공통 Agent Task Runtime과 Desktop Agent - - 경로: [shared-agent-task-runtime-desktop-agent](../../archive/phase/automation-runtime-bridge/milestones/shared-agent-task-runtime-desktop-agent.md) + - 경로: [shared-agent-task-runtime-desktop-agent](milestones/shared-agent-task-runtime-desktop-agent.md) - 요약: 공통 runtime·Desktop host·Flutter 배포를 결합한 기존 범위는 IOP Agent CLI Runtime으로 분할한 뒤 후속 Flutter·Unity 제품 계획을 Chronos로 이전해 독립 구현 단위로 폐기했다. - [완료] 워크스페이스 포트/환경 표준화 - - 경로: [workspace-port-env-standardization](../../archive/phase/automation-runtime-bridge/milestones/workspace-port-env-standardization.md) + - 경로: [workspace-port-env-standardization](milestones/workspace-port-env-standardization.md) - 요약: Control Plane, Edge, Node, Client, OpenAI-compatible, A2A, wire, metrics, DB/cache 포트를 workspace 공통 대역으로 정렬한다. - [완료] 브리지 선행 경계 안정화 - - 경로: [bridge-boundary-hardening](../../archive/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md) + - 경로: [bridge-boundary-hardening](milestones/bridge-boundary-hardening.md) - 요약: 원격 터미널 브리지 POC 전에 남은 호환성/소유권 리스크를 Client HTTP lifecycle, Edge run surface, Node terminal core, typed adapter config 계약으로 고정한다. - [완료] Codex App Server 스트리밍 전환 - - 경로: [codex-app-server-streaming-migration](../../archive/phase/automation-runtime-bridge/milestones/codex-app-server-streaming-migration.md) + - 경로: [codex-app-server-streaming-migration](milestones/codex-app-server-streaming-migration.md) - 요약: Codex CLI target의 기본 `codex` profile을 app-server 기반으로 전환하고 실제 `codex` foreground/background smoke와 app-server session lifecycle 검증을 완료했다. - [완료] OpenAI Workspace Agent Execution Contract - - 경로: [openai-workspace-agent-execution-contract](../../archive/phase/automation-runtime-bridge/milestones/openai-workspace-agent-execution-contract.md) + - 경로: [openai-workspace-agent-execution-contract](milestones/openai-workspace-agent-execution-contract.md) - 요약: NomadCode가 IOP CLI를 직접 실행하지 않고 IOP Edge OpenAI-compatible HTTP 호출의 `metadata.workspace`와 task/source metadata만으로 내부 workspace-bound agent target이 해당 checkout에서 산출물을 만들 수 있게 하는 최우선 contract/serving hardening 작업이다. - [완료] 에이전트 작업성 중심 저장소 구조 리팩터링 - - 경로: [agent-readable-repository-refactor](../../archive/phase/automation-runtime-bridge/milestones/agent-readable-repository-refactor.md) + - 경로: [agent-readable-repository-refactor](milestones/agent-readable-repository-refactor.md) - 요약: 거대 소스·테스트와 추적 artifact를 책임 단위로 정리하고, task-local read set과 동작 보존형 가독성 ratchet을 검증해 완료했다. - [완료] Agent Task 동적 실행 Target Selector - - 경로: [agent-task-runtime-target-selector](../../archive/phase/automation-runtime-bridge/milestones/agent-task-runtime-target-selector.md) + - 경로: [agent-task-runtime-target-selector](milestones/agent-task-runtime-target-selector.md) - 요약: 정적 lane/G 결과를 시간대, quota와 이전 실행 상태에 결합해 작업 단위로 고정되는 `adapter + target` 선택과 failover·selfcheck·task-local blocker·독립 작업 drain 정책을 구현했다. 중복된 최종 audit pair는 임시 Python 구현 폐기 예정에 따른 사용자 결정으로 미실행 종료했다. - [폐기] Pi CLI Provider Integration - - 경로: [pi-cli-provider-integration](../../archive/phase/automation-runtime-bridge/milestones/pi-cli-provider-integration.md) + - 경로: [pi-cli-provider-integration](milestones/pi-cli-provider-integration.md) - 요약: workspace와 tools를 가진 Pi CLI agent 실행은 IOP 추론 provider 책임이 아니므로 IOP 계획을 폐기하고 Chronos Server/Node의 agent 실행 후보로 넘긴다. - [완료] IOP Agent CLI Runtime - - 경로: [iop-agent-cli-runtime](../../archive/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md) + - 경로: [iop-agent-cli-runtime](milestones/iop-agent-cli-runtime.md) - 요약: Python 감시·dispatcher와 Node CLI runtime 동등성을 공통 Go CLI Provider·AgentTaskManager 및 개인 장비당 단일 `iop-agent` binary로 이전하고, 다중 project 관측·수동 시작/자동 재개·client subprocess 소유 경계를 완료했다. - [완료] [separation-01] IOP Agent Runtime의 Chronos 전체 이전과 IOP 의존성 제거 - - 경로: [[separation-01] IOP Agent Runtime의 Chronos 전체 이전과 IOP 의존성 제거](../../archive/phase/automation-runtime-bridge/milestones/iop-agent-chronos-extraction-decoupling.md) + - 경로: [[separation-01] IOP Agent Runtime의 Chronos 전체 이전과 IOP 의존성 제거](milestones/iop-agent-chronos-extraction-decoupling.md) - 요약: `iop-agent` 자산과 상태를 repository-neutral Chronos acceptance bundle로 전달하고 IOP의 agent·terminal·workspace·Chronos 연결 surface를 제거했다. canonical `dev`의 provider-only 회귀와 Agent UI 정합화가 통과해 Chronos 선행 잠금을 활성화했다. - [폐기] oto 자동화 스케줄러와 CI-CD 연동 (2차) - - 경로: [oto-automation-scheduler-second-wave](../../archive/phase/automation-runtime-bridge/milestones/oto-automation-scheduler-second-wave.md) + - 경로: [oto-automation-scheduler-second-wave](milestones/oto-automation-scheduler-second-wave.md) - 요약: loop engineering, scheduler와 CI-CD workflow는 IOP가 아니라 Chronos Server 책임이므로 IOP 후보를 폐기한다. - [폐기] 원격 터미널/CLI 터널링 POC (2차) - - 경로: [remote-terminal-bridge-poc](../../archive/phase/automation-runtime-bridge/milestones/remote-terminal-bridge-poc.md) + - 경로: [remote-terminal-bridge-poc](milestones/remote-terminal-bridge-poc.md) - 요약: terminal/PTY와 원격 호스트 제어는 독립 Chronos Node가 소유하므로 Edge-IOP Node 브리지 방식의 IOP POC를 폐기한다. ## Phase 경계 - OpenAI-compatible, Anthropic-compatible와 A2A 호환 표면은 IOP의 추론 provider 호출에 한정하며 terminal·workspace·agent loop 제어를 싣지 않는다. - Edge는 IOP provider 요청의 broker 역할을 하고, IOP Node는 model/provider/device 실행자 역할만 유지한다. -- 완료된 `iop-agent`의 Edge 비의존 headless CLI, 단일 active supervisor와 same-user local-control 동작은 전체 이전 기준선이다. IOP 선행 분리 Milestone이 끝나면 IOP에는 해당 binary, supervisor/client lifecycle, 작업 상태와 관련 계약을 남기지 않는다. +- 완료된 `iop-agent`의 Edge 비의존 headless CLI, 단일 active supervisor와 same-user local-control 동작은 전체 이전 기준선이며, IOP에는 해당 binary, supervisor/client lifecycle, 작업 상태와 관련 계약을 남기지 않는다. - 단일 `iop-agent`의 기존 project 관측·client process 기능과 저장 상태는 IOP 선행 Milestone의 전체 이전 목록과 동작 검증 입력으로 취급하고, IOP에 새 client lifecycle·local control 기능을 추가하지 않는다. - OpenAI-compatible Responses 표면은 외부 모델 호출 호환을 위한 입력 표면이며, IOP 고유 provider 운영 제어는 native protocol이나 명시 운영 API로 분리한다. `metadata.workspace`는 IOP Node의 workspace agent 실행이나 원격 mutation 권한으로 해석하지 않는다. - 완료된 `iop-agent`와 공통 Agent Task runtime의 작업공간 보호, 대상 선택, 복구, 상태·검토·반영 동작은 Chronos로 전부 이전한다. IOP Node에는 IOP가 소유한 provider 실행·취소·상태·usage와 model/device lifecycle만 유지한다. - Plan/Review·Milestone·Roadmap lifecycle, 일반 요청 triage, task filename lane/grade 해석, route policy, terminal/PTY, file/process와 원격 workspace 제어는 Chronos Server와 Chronos Node가 소유한다. - Chronos Node는 IOP Node와 별도 runtime·identity·registry·wire를 가진다. IOP에는 Chronos를 위한 bridge/API/proto/config, target 등록, forwarding runtime 또는 future control hook을 남기지 않는다. - Chronos가 모델 추론이 필요하면 IOP의 공개 추론 API를 일반 client로 호출한다. IOP는 Chronos 작업/session/node 의미를 알지 못하고 Chronos는 IOP Node를 제어하지 않는다. -- 외부 `model=iop`으로 명시 선택되는 [IOP Hot Path One-shot 실행 경로](../knowledge-tool-optimization-extension/milestones/iop-hot-path-one-shot-execution.md)는 IOP가 계속 소유하는 독립 경로이며 Chronos의 direct/Plan/Milestone 분류, durable artifact와 continuation을 거치거나 공유하지 않는다. +- 외부 `model=iop`으로 명시 선택되는 [IOP Hot Path One-shot 실행 경로](../../../phase/knowledge-tool-optimization-extension/milestones/iop-hot-path-one-shot-execution.md)는 IOP가 계속 소유하는 독립 경로이며 Chronos의 direct/Plan/Milestone 분류, durable artifact와 continuation을 거치거나 공유하지 않는다. - IOP가 quota/status/failure event를 제공할 수는 있지만 macOS/Desktop 알림 delivery와 이력은 Chronos가 소유한다. - Pi 같은 workspace CLI agent, 원격 터미널/CLI 터널링과 oto scheduler/CI-CD는 IOP 재개 후보로 두지 않고 Chronos 책임에서 새로 설계한다. diff --git a/agent-spec/index.md b/agent-spec/index.md index 444158a6..001a8904 100644 --- a/agent-spec/index.md +++ b/agent-spec/index.md @@ -22,7 +22,7 @@ AI agent가 작업 전에 읽는 지도이기도 하지만, 사람도 "지금 ## 영역별 요약 -- 실행 경로: Edge와 Node 사이의 TLS identity, 등록, 실행, 이벤트, provider raw tunnel, sealed credential lease consumption, 취소, command 흐름은 `runtime/edge-node-execution`에서 본다. +- 실행 경로: Edge와 Node 사이의 TLS identity, 등록, 실행, 이벤트, provider raw tunnel, transport heartbeat/reconnect, sealed credential lease consumption, 취소, command 흐름은 `runtime/edge-node-execution`에서 본다. - 런타임 라우팅/설정: provider-pool, managed credential mode, `models[]`, top-level `protocol_profiles`, `nodes[].providers[].profile`, 그리고 refresh classification은 `runtime/provider-pool-config-refresh`에서 본다. - 출력 검증 런타임: staged response-start, evidence hold/release, filter arbitration, bounded recovery/rebuild, raw-free observation은 `runtime/stream-evidence-gate`에서 본다. - 외부 HTTP 입력: OpenAI-compatible 호출, Anthropic-compatible Messages 호출, managed principal route/slot binding, model-driven raw tunnel은 `input/openai-compatible-surface`, A2A JSON-RPC 호출은 `input/a2a-json-rpc-surface`에서 본다. @@ -32,7 +32,7 @@ AI agent가 작업 전에 읽는 지도이기도 하지만, 사람도 "지금 | id | 상태 | 언제 읽나 | path | 주요 근거 | |----|------|-----------|------|-----------| -| `runtime/edge-node-execution` | 부분 | Edge-Node mTLS/protobuf transport, Node 등록, provider run/cancel/command, provider raw tunnel, signed/sealed credential lease consumption, execution registry와 Node local run store를 확인할 때 | `agent-spec/runtime/edge-node-execution.md` | `agent-contract/inner/execution-runtime.md`, `agent-contract/inner/edge-node-runtime-wire.md`, `apps/node/internal/node/runtime_bridge.go` | +| `runtime/edge-node-execution` | 구현됨 | Edge-Node mTLS/protobuf transport, Node 등록, transport heartbeat/reconnect, provider run/cancel/command, provider raw tunnel, signed/sealed credential lease consumption을 확인할 때 | `agent-spec/runtime/edge-node-execution.md` | `agent-contract/inner/execution-runtime.md`, `agent-contract/inner/edge-node-runtime-wire.md`, `apps/edge/internal/transport/server.go`, `apps/node/internal/transport/client.go` | | `runtime/stream-evidence-gate` | 구현됨 | Stream Evidence Gate의 normalized event, evidence hold/release, filter registry, recovery coordinator, OpenAI request rebuild와 observation을 확인할 때 | `agent-spec/runtime/stream-evidence-gate.md` | `packages/go/streamgate/runtime.go`, `apps/edge/internal/openai/stream_gate_runtime.go`, `agent-contract/outer/openai-compatible-api.md` | | `runtime/provider-pool-config-refresh` | 부분 | `credential_plane`, managed/legacy exclusivity, TLS/key references, `models[]`, top-level `protocol_profiles`, `nodes[].providers[].profile`, provider-pool dispatch, long-context admission, and restart/applied refresh classification을 확인할 때 | `agent-spec/runtime/provider-pool-config-refresh.md` | `agent-contract/inner/edge-config-runtime-refresh.md`, `packages/go/config/provider_types.go`, `packages/go/config/validate.go`, `apps/edge/internal/configrefresh/classify.go` | | `input/openai-compatible-surface` | 부분 | `/v1/models`, `/v1/chat/completions`, `/v1/responses`, `/v1/messages`, `/v1/messages/count_tokens`, `/anthropic/v1/models`, managed projection/slot routing, OpenAI-compatible auth/metadata/tool handling, Anthropic bearer/`X-Api-Key` auth, provider-pool native/bridge admission, safe slot attribution, and OpenAI-only usage metrics를 확인할 때 | `agent-spec/input/openai-compatible-surface.md` | `agent-contract/outer/openai-compatible-api.md`, `agent-contract/outer/anthropic-compatible-api.md`, `apps/edge/internal/openai/chat_handler.go`, `apps/edge/internal/openai/anthropic_handler.go`, `apps/edge/internal/openai/anthropic_bridge.go`, `apps/edge/internal/openai/normalized_sse.go`, `apps/edge/internal/openai/usage_metrics.go` | diff --git a/agent-spec/runtime/edge-node-execution.md b/agent-spec/runtime/edge-node-execution.md index c765e0d8..b2c3fdc4 100644 --- a/agent-spec/runtime/edge-node-execution.md +++ b/agent-spec/runtime/edge-node-execution.md @@ -15,6 +15,12 @@ source_evidence: - type: code path: apps/node/internal/node/runtime_bridge.go notes: Protobuf-to-execution translation + - type: code + path: apps/edge/internal/transport/server.go + notes: Edge-side tunnel-tolerant heartbeat and disconnect supervision + - type: code + path: apps/node/internal/transport/client.go + notes: Node-side tunnel-tolerant heartbeat and reconnect transport - type: code path: apps/edge/internal/service/provider_tunnel.go notes: Provider selection, credential binding validation, lease acquisition, and pre-send fencing @@ -27,36 +33,92 @@ source_evidence: - type: test path: apps/node/internal/node/command_test.go notes: Closed provider commands, correlation, and cancellation regressions + - type: test + path: apps/edge/internal/transport/heartbeat_test.go + notes: Edge heartbeat liveness profile regression + - type: test + path: apps/node/internal/transport/heartbeat_test.go + notes: Node heartbeat liveness and idle-connection regressions --- # Edge-Node Provider Execution -## Current implementation +## 목적 Edge owns provider selection, queue admission, leases, and connection-generation fencing. Node owns local provider adapters and executes normalized runs or provider HTTP tunnels after a ready handshake. The shared `packages/go/execution` package contains provider lifecycle, registry, execution events, failures, cancellation, usage, and optional tunnel/command primitives. It does not manage host programs or durable conversation state. -## Runtime rules +## 기능 목록 -- A registered Node is not dispatchable until its current connection completes readiness. -- `adapter + target` selects provider execution. -- `session_id` is copied through events and command results as opaque correlation only. -- Every run is independent, including repeated correlation values. -- Cancellation requires `run_id` and affects that run only. -- Node commands are limited to capabilities, transport status, and the Ollama API tunnel. -- Run and tunnel streams preserve ordering and exactly one terminal outcome. -- Provider usage, capacity, queue pressure, lifecycle, reconnect, and tool calling remain supported. -- Managed mode requires CA-validated Edge and Node workload identities before registration or dispatch. -- Edge binds the selected principal, route, slot, profile, upstream target, Node recipient, revisions, and projection generation before acquiring a short-lived sealed lease. -- Node opens the lease only after adapter-capacity admission and immediately before provider execution, injects the declared auth header in memory, and zeroes plaintext afterward. -- Revoked, disabled, expired, stale, replayed, wrong-recipient, or mismatched leases fail closed without provider or credential fallback. +| 기능 | 설명 | +|------|------| +| register/readiness | 등록된 Node의 현재 connection이 readiness를 완료한 뒤에만 dispatch한다. | +| normalized execution | `adapter + target`으로 provider 실행을 선택하고 ordered `RunEvent` stream을 반환한다. | +| provider raw tunnel | 선택된 provider의 HTTP/SSE를 `ProviderTunnelRequest`/`ProviderTunnelFrame`으로 relay하며 순서와 단일 terminal outcome을 보장한다. | +| tunnel-tolerant liveness | Edge와 Node는 30초 heartbeat interval과 45초 response wait를 공통으로 사용해 긴 prompt prefill이나 streaming backpressure 중의 정상 connection을 조기에 끊지 않는다. | +| reconnect/generation fencing | 현재 connection이 종료되면 해당 generation만 fence하고 Node supervisor가 reconnect한다. Heartbeat wait를 넘긴 경우의 close reason은 `heartbeat_timeout`이다. | +| cancellation/command | `run_id`로 현재 run만 취소하며 command는 capabilities, transport status, Ollama API tunnel로 제한한다. | +| managed credential lease | Edge가 principal·route·slot·profile·target·Node·revision·generation을 binding한 sealed lease를 발급하고 Node가 capacity admission 후 provider 실행 직전에만 연다. | -## Removed ownership +## 범위 + +- `session_id`는 event와 command result의 opaque correlation일 뿐이며 같은 값을 재사용해도 모든 run은 독립적이다. +- provider usage, capacity, queue pressure, lifecycle, reconnect, tool calling은 Edge-Node 실행 경로에서 계속 지원한다. +- managed mode는 등록과 dispatch 전에 CA로 검증된 Edge/Node workload identity를 요구한다. +- revoked, disabled, expired, stale, replayed, wrong-recipient, mismatched lease는 provider나 credential fallback 없이 fail closed한다. IOP no longer provides persistent shell sessions, terminal emulation, process resume, local working-directory execution context, arbitrary host commands, or local quota/status probing. -## Verification +## 주요 흐름 + +```mermaid +sequenceDiagram + participant Edge + participant Node + participant Provider + + Node->>Edge: RegisterRequest + Edge-->>Node: RegisterResponse + config + Node->>Edge: NodeReadyRequest + Edge-->>Node: NodeReadyResponse + Edge->>Node: ProviderTunnelRequest + Node->>Provider: HTTP/SSE request + Provider-->>Node: status/header/body stream + Node-->>Edge: ordered ProviderTunnelFrame stream + Note over Edge,Node: heartbeat 30s interval / 45s wait + alt heartbeat wait exceeded + Edge--xNode: current generation fenced + Node->>Edge: supervised reconnect + end +``` + +## 계약 + +- Edge-Node wire: `agent-contract/inner/edge-node-runtime-wire.md` +- provider execution primitives: `agent-contract/inner/execution-runtime.md` + +Heartbeat interval/wait는 protobuf field가 아닌 양쪽 transport 구현의 liveness profile이다. Wire message와 provider response shape은 바뀌지 않는다. + +## 설정/데이터/이벤트 + +- Edge와 Node의 현재 heartbeat interval은 30초, response wait는 45초다. +- 이 값은 runtime YAML model config나 `max_tokens`/context 설정이 아니라 transport 구현 상수다. +- 45초 동안 heartbeat response가 없으면 current connection을 `heartbeat_timeout`으로 닫고 provider resource를 offline 처리한 뒤 reconnect/queue 재평가를 수행한다. + +## 검증 - `go test -count=1 ./packages/go/execution ./apps/node/... ./apps/edge/internal/service` - `go test -race -count=1 ./packages/go/execution ./apps/node/internal/node ./apps/edge/internal/service` +- `go test -count=1 ./apps/node/internal/transport ./apps/edge/internal/transport` +- `go test -race -count=1 ./apps/node/internal/transport ./apps/edge/internal/transport` +- 실제 provider tunnel 검증은 5초를 넘는 긴 prefill과 streaming 응답 동안 Node가 connected/healthy를 유지하고, 응답이 정상 terminal을 반환하며, `heartbeat_timeout`이 발생하지 않는지 확인한다. + +## 한계와 주의사항 + +- 30/45초 liveness profile은 provider 응답 token 상한이나 model context window를 늘리지 않는다. 요청 중단 원인 판정 시 model 설정과 transport disconnect를 별도로 확인한다. +- 45초를 넘겨 실제 heartbeat response가 없는 connection은 기존과 같이 오프라인 처리하고 reconnect한다. + +## 변경 기록 + +- 2026-08-02: provider tunnel의 긴 prompt prefill과 streaming backpressure를 정상 traffic으로 허용하도록 Edge/Node heartbeat profile을 30초 interval/45초 wait로 복원한 현재 구현과 회귀 검증을 반영했다 (`apps/edge/internal/transport/server.go`, `apps/node/internal/transport/client.go`). diff --git a/agent-task/m-iop-agent-chronos-extraction-decoupling/WORK_LOG.md b/agent-task/m-iop-agent-chronos-extraction-decoupling/WORK_LOG.md deleted file mode 100644 index 49b73567..00000000 --- a/agent-task/m-iop-agent-chronos-extraction-decoupling/WORK_LOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# Milestone Work Log - -> Dispatcher-owned execution timeline. Workers and reviewers do not edit this file. - -| seq | time | event | task | loop | role | attempt | model | result | locator | -|---:|---|---|---|---:|---|---:|---|---|---| -| 1 | 26-08-02 19:23:48 | START | m-iop-agent-chronos-extraction-decoupling/17+15,16_canonical_promotion_closure/PLAN-cloud-G10.md | 0 | worker | 0 | codex/gpt-5.6-sol xhigh | running | /config/workspace/iop-s1/.git/agent-task-dispatcher/runs/20260802T102348Z__m-iop-agent-chronos-extraction-decoupling__17__15__16_canonical_promotion_closure__p0__worker__a00/locator.json | diff --git a/agent-ui/definition/components/console-shell/index.md b/agent-ui/definition/components/console-shell/index.md index 588baa35..aafe5bc0 100644 --- a/agent-ui/definition/components/console-shell/index.md +++ b/agent-ui/definition/components/console-shell/index.md @@ -10,7 +10,7 @@ source_evidence: path: apps/client/README.md notes: iop_console is the IOP-owned widget boundary for standalone and embedded consumers. - type: docs - path: agent-roadmap/sdd/automation-runtime-bridge/iop-agent-chronos-extraction-decoupling/SDD.md + path: agent-roadmap/archive/sdd/automation-runtime-bridge/iop-agent-chronos-extraction-decoupling/SDD.md notes: The approved SDD removes the standalone Agent section while retaining generic IOP operations sections. - type: user path: null @@ -29,7 +29,7 @@ Status: `구현됨` |------|------|-------| | code | `packages/flutter/iop_console/lib/src/iop_console_shell.dart` | `IopConsoleShell`, `_IopConsoleRail`, `_RailButton`, `_IopPlaceholder`가 shell structure를 구현한다. | | docs | `apps/client/README.md` | standalone IOP app과 외부 embedding consumer가 같은 widget boundary를 사용한다고 설명한다. | -| docs | `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-chronos-extraction-decoupling/SDD.md` | Agent section은 제거하고 범용 IOP 운영 section은 유지하도록 확정한다. | +| docs | `agent-roadmap/archive/sdd/automation-runtime-bridge/iop-agent-chronos-extraction-decoupling/SDD.md` | Agent section은 제거하고 범용 IOP 운영 section은 유지하도록 확정한다. | | user | 없음 | 초기 agent-ui 생성을 요청했다. | ## Purpose diff --git a/apps/edge/README.md b/apps/edge/README.md index 5c418a14..7ee93682 100644 --- a/apps/edge/README.md +++ b/apps/edge/README.md @@ -11,6 +11,7 @@ IOP Edge owns Node registration, dispatch readiness, provider-pool routing, queu - Edge operations are `health.check`, `node.status`, and `provider.command`. - Provider commands are limited to capabilities, transport status, and the Ollama API tunnel. - Caller metadata remains an ordinary bounded string map. +- Edge and Node use a shared tunnel-tolerant liveness profile: a 30-second heartbeat interval and a 45-second response wait. Long provider prefill or streamed-response backpressure does not prematurely fence a healthy connection; a real timeout still closes only the current generation and triggers Node reconnect. Edge and Node preserve standard OpenAI/Anthropic-compatible inference, streaming, tools, usage, provider lifecycle, capacity, queues, and reconnect behavior. They do not own interactive host programs, persistent conversations, terminal emulation, arbitrary host commands, or local filesystem execution context. diff --git a/apps/edge/internal/transport/heartbeat_test.go b/apps/edge/internal/transport/heartbeat_test.go index 7d1fd7dd..1c54433f 100644 --- a/apps/edge/internal/transport/heartbeat_test.go +++ b/apps/edge/internal/transport/heartbeat_test.go @@ -13,11 +13,11 @@ func TestHeartbeatWaitExceedsInterval(t *testing.T) { } } -func TestHeartbeatUsesLowLatencyNodeLivenessProfile(t *testing.T) { - if heartbeatIntervalSec != 2 { - t.Fatalf("heartbeatIntervalSec: got %d want 2", heartbeatIntervalSec) +func TestHeartbeatUsesTunnelTolerantLivenessProfile(t *testing.T) { + if heartbeatIntervalSec != 30 { + t.Fatalf("heartbeatIntervalSec: got %d want 30", heartbeatIntervalSec) } - if heartbeatWaitSec != 5 { - t.Fatalf("heartbeatWaitSec: got %d want 5", heartbeatWaitSec) + if heartbeatWaitSec != 45 { + t.Fatalf("heartbeatWaitSec: got %d want 45", heartbeatWaitSec) } } diff --git a/apps/edge/internal/transport/server.go b/apps/edge/internal/transport/server.go index 3d2f9bf9..59d002ae 100644 --- a/apps/edge/internal/transport/server.go +++ b/apps/edge/internal/transport/server.go @@ -20,10 +20,13 @@ import ( ) const ( - heartbeatIntervalSec = 2 + // Match the Node's tunnel-tolerant liveness profile. Long provider prefill and + // streamed-response backpressure must not fence a healthy connection before + // it can service the next heartbeat. + heartbeatIntervalSec = 30 // heartbeatWaitSec mirrors the node side. See the comment in // apps/node/internal/transport/client.go for rationale. - heartbeatWaitSec = 5 + heartbeatWaitSec = 45 ) const configRefreshTimeoutSec = 30 diff --git a/apps/node/README.md b/apps/node/README.md index d0c21f75..9995a2bb 100644 --- a/apps/node/README.md +++ b/apps/node/README.md @@ -9,6 +9,7 @@ IOP Node connects to Edge, applies the provider configuration delivered during r - Cancellation requires a non-empty `run_id` and affects only that run. - Provider commands are limited to capabilities, transport status, and the Ollama API tunnel. - Standard inference, streaming, structured tools, usage, provider lifecycle, capacity, and reconnect behavior remain supported. +- Edge and Node use a shared tunnel-tolerant liveness profile: a 30-second heartbeat interval and a 45-second response wait. This keeps long provider prefill or streamed-response backpressure from disconnecting a healthy Node; a connection that actually exceeds the wait closes as `heartbeat_timeout` and enters supervised reconnect. Node does not manage interactive host programs, persistent conversations, terminal emulation, arbitrary host commands, local filesystem execution context, or local quota scraping. diff --git a/apps/node/internal/transport/client.go b/apps/node/internal/transport/client.go index 6fb82186..ef024e08 100644 --- a/apps/node/internal/transport/client.go +++ b/apps/node/internal/transport/client.go @@ -74,13 +74,17 @@ func IsFatalConnectError(err error) bool { } const ( - heartbeatIntervalSec = 2 + // Provider tunnel traffic can legitimately delay heartbeat processing while a + // long prompt is in prefill or a streamed response is applying backpressure. + // Keep the transport liveness window above those request-local stalls so an + // otherwise healthy field node is not disconnected mid-generation. + heartbeatIntervalSec = 30 // heartbeatWaitSec is kept above heartbeatIntervalSec as defence in depth: // the library wait-timer callback already self-heals stale state (see // proto-socket go/base_client.go sendHeartBeat), but a larger wait window // gives the peer's next heartbeat an extra chance to overwrite any stray // timer on slow or jittery links before it fires. - heartbeatWaitSec = 5 + heartbeatWaitSec = 45 tcpWriteTimeout = 10 * time.Second registerTimeout = 10 * time.Second registerInitialWait = 100 * time.Millisecond diff --git a/apps/node/internal/transport/heartbeat_test.go b/apps/node/internal/transport/heartbeat_test.go index 13ba4c45..cf11bca3 100644 --- a/apps/node/internal/transport/heartbeat_test.go +++ b/apps/node/internal/transport/heartbeat_test.go @@ -155,12 +155,12 @@ func TestHeartbeatWaitExceedsInterval(t *testing.T) { } } -func TestHeartbeatUsesLowLatencyNodeLivenessProfile(t *testing.T) { - if heartbeatIntervalSec != 2 { - t.Fatalf("heartbeatIntervalSec: got %d want 2", heartbeatIntervalSec) +func TestHeartbeatUsesTunnelTolerantLivenessProfile(t *testing.T) { + if heartbeatIntervalSec != 30 { + t.Fatalf("heartbeatIntervalSec: got %d want 30", heartbeatIntervalSec) } - if heartbeatWaitSec != 5 { - t.Fatalf("heartbeatWaitSec: got %d want 5", heartbeatWaitSec) + if heartbeatWaitSec != 45 { + t.Fatalf("heartbeatWaitSec: got %d want 45", heartbeatWaitSec) } } diff --git a/scripts/fixtures/iop-agent-chronos-transfer-state-v1.contract-test.jq b/scripts/fixtures/iop-agent-chronos-transfer-state-v1.contract-test.jq deleted file mode 100644 index e9f4380d..00000000 --- a/scripts/fixtures/iop-agent-chronos-transfer-state-v1.contract-test.jq +++ /dev/null @@ -1,133 +0,0 @@ -# Schema-contract regression for iop-agent-chronos-transfer-state-v1. -# Reads the schema and fixture, enforces the read-only + quarantine/resumability -# contract, validates schema-clause shapes, accepts the canonical fixture, -# and rejects counterexample record and schema mutations. -# -# Usage: -# jq -nr \ -# --slurpfile schema scripts/fixtures/iop-agent-chronos-transfer-state-v1.schema.json \ -# --slurpfile fixture scripts/fixtures/iop-agent-chronos-transfer-state-v1.json \ -# -f scripts/fixtures/iop-agent-chronos-transfer-state-v1.contract-test.jq - -def validate_schema($s): - ($s | .["$defs"].record) as $rec - | (($rec.properties.mode | keys) == ["const", "description", "type"]) - and ($rec.properties.mode.const == "read-only") - and (($rec.properties.category | keys) == ["description", "enum", "type"]) - and ($rec.properties.category.type == "string") - and ($rec.properties.category.enum == ["config", "state", "client", "log", "overlay", "cache", "temp", "quarantine"]) - and (($rec.properties.resumable | keys) == ["description", "type"]) - and ($rec.properties.resumable.type == "boolean") - and (($rec.properties.quarantine_reason | keys) == ["description", "minLength", "type"]) - and ($rec.properties.quarantine_reason.minLength == 1) - and ($rec.allOf != null and ($rec.allOf | length) == 1) - and ($rec.allOf[0] as $cond - | ($cond.then.properties | keys) as $then_keys - | ($cond.else.properties | keys) as $else_keys - | ($cond | keys) == ["else", "if", "then"] - and ($cond.if | keys) == ["properties", "required"] - and ($cond.if.properties | keys) == ["category"] - and (($cond.if.properties.category | keys) == ["const"]) - and ($cond.if.properties.category.const == "quarantine") - and ($cond.if.required == ["category"]) - and ($cond.then | keys) == ["properties", "required"] - and ($then_keys == ["resumable"]) - and ($else_keys == $then_keys) - and (($cond.then.properties[$then_keys[0]] | keys) == ["const"]) - and ($cond.then.properties[$then_keys[0]].const == false) - and ($cond.then.required == ["quarantine_reason"]) - and ($cond.else | keys) == ["not", "properties"] - and (($cond.else.properties[$else_keys[0]] | keys) == ["const"]) - and ($cond.else.properties[$else_keys[0]].const == true) - and ($cond.else.not | keys) == ["required"] - and ($cond.else.not.required == ["quarantine_reason"])); - -def schema_contract($s): - if validate_schema($s) then - ($s | .["$defs"].record) as $rec | - ($rec.allOf[0]) as $cond | - ($cond.then.properties | keys) as $then_keys | - { - valid: true, - category_key: $cond.if.required[0], - quarantine_cat: $cond.if.properties.category.const, - mode_const: $rec.properties.mode.const, - min_length: $rec.properties.quarantine_reason.minLength, - resumable_key: $then_keys[0], - quarantine_resumable: $cond.then.properties[$then_keys[0]].const, - non_quarantine_resumable: $cond.else.properties[$then_keys[0]].const, - required_reason_key: $cond.then.required[0], - forbidden_reason_key: $cond.else.not.required[0] - } - else - { valid: false } - end; - -def check_record($contract; $record): - if ($contract.valid | not) then - false - else - ($record.mode == $contract.mode_const) - and (if $record[$contract.category_key] == $contract.quarantine_cat then - ($record[$contract.resumable_key] == $contract.quarantine_resumable) - and ($record | has($contract.required_reason_key)) - and (($record[$contract.required_reason_key] | length) >= $contract.min_length) - else - ($record[$contract.resumable_key] == $contract.non_quarantine_resumable) - and ($record | has($contract.forbidden_reason_key) | not) - end) - end; - -# Extract schema contract from provided schema doc -schema_contract($schema[0]) as $contract -| -# Internal schema clause mutation tests -(schema_contract($schema[0] | .["$defs"].record.allOf = []) | .valid | not) as $schema_neg1 -| (schema_contract($schema[0] | .["$defs"].record.properties.mode.const = "read-write") | .valid | not) as $schema_neg2 -| (schema_contract($schema[0] | .["$defs"].record.properties.quarantine_reason.minLength = 0) | .valid | not) as $schema_neg3 -| (schema_contract($schema[0] | .["$defs"].record.allOf[0].then.properties.resumable.const = true) | .valid | not) as $schema_neg4 -| (schema_contract($schema[0] | .["$defs"].record.allOf[0].if.required += ["missing_guard"]) | .valid | not) as $schema_neg5 -| (schema_contract($schema[0] | .["$defs"].record.allOf[0].then.required += ["missing_required"]) | .valid | not) as $schema_neg6 -| (schema_contract($schema[0] | .["$defs"].record.allOf[0].else.not.required += ["missing_forbidden_guard"]) | .valid | not) as $schema_neg7 -| (schema_contract($schema[0] | .["$defs"].record.allOf += [{"not": {}}]) | .valid | not) as $schema_neg8 -| (schema_contract($schema[0] | .["$defs"].record.allOf[0].if.properties.category.pattern = "^never$") | .valid | not) as $schema_neg9 -| (schema_contract($schema[0] | .["$defs"].record.allOf[0].then.properties.resumable.not = {}) | .valid | not) as $schema_neg10 -| (schema_contract($schema[0] | .["$defs"].record.allOf[0].else.properties.resumable.not = {}) | .valid | not) as $schema_neg11 -| (schema_contract($schema[0] | .["$defs"].record.properties.quarantine_reason.maxLength = 0) | .valid | not) as $schema_neg12 -| (schema_contract($schema[0] | .["$defs"].record.properties.category.const = "quarantine") | .valid | not) as $schema_neg13 -| (schema_contract($schema[0] | .["$defs"].record.properties.resumable.const = false) | .valid | not) as $schema_neg14 -| -# --- Positive case: canonical fixture --- -($fixture[0].records | map(check_record($contract; .)) | all) as $positive_pass -| -# --- Negative cases: counterexample record mutations --- -# 1. mode=read-write -({version:1, records:[($fixture[0].records[0] | .mode = "read-write")]}) as $neg1_data -| ($neg1_data.records | map(check_record($contract; .)) | all) as $neg1 -| -# 2. non-quarantine resumable=false -({version:1, records:[($fixture[0].records[0] | .resumable = false)]}) as $neg2_data -| ($neg2_data.records | map(check_record($contract; .)) | all) as $neg2 -| -# 3. quarantine resumable=true (use the quarantine record at index 7) -({version:1, records:[($fixture[0].records[7] | .resumable = true)]}) as $neg3_data -| ($neg3_data.records | map(check_record($contract; .)) | all) as $neg3 -| -# 4. non-quarantine quarantine_reason -({version:1, records:[($fixture[0].records[0] | .quarantine_reason = "test")]}) as $neg4_data -| ($neg4_data.records | map(check_record($contract; .)) | all) as $neg4 -| -# 5. quarantine record with empty quarantine_reason -({version:1, records:[($fixture[0].records[7] | .quarantine_reason = "")]}) as $neg5_data -| ($neg5_data.records | map(check_record($contract; .)) | all) as $neg5 -| -# --- Aggregate result --- -if $contract.valid - and $positive_pass - and ([$neg1, $neg2, $neg3, $neg4, $neg5] | all(. == false)) - and ([$schema_neg1, $schema_neg2, $schema_neg3, $schema_neg4, $schema_neg5, $schema_neg6, $schema_neg7, $schema_neg8, $schema_neg9, $schema_neg10, $schema_neg11, $schema_neg12, $schema_neg13, $schema_neg14] | all(. == true)) -then - "STATE_SCHEMA_CONTRACT_PASS" -else - error("state schema contract regression failed") -end