merge(agent-runtime): dev 최신 변경을 반영한다
This commit is contained in:
commit
b777525e96
9 changed files with 1221 additions and 4 deletions
|
|
@ -0,0 +1,148 @@
|
|||
<!-- task=provider_stream_buffer_compat plan=0 tag=STREAM_BUFFER -->
|
||||
|
||||
# Code Review Reference - STREAM_BUFFER
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `Implementation Checklist`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence fields.
|
||||
> Do not ask the user directly, present choices, call user-input tools, create control-plane stop files, or classify the next state.
|
||||
> Finalization (`Code Review Result`, log rename, `complete.log`, archive moves, `Review-Only Checklist`) is review-agent-only, even after compaction/resume.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## Overview
|
||||
|
||||
date=2026-07-30
|
||||
task=provider_stream_buffer_compat, plan=0, tag=STREAM_BUFFER
|
||||
|
||||
## For the Review Agent
|
||||
|
||||
> **[REVIEW AGENT ONLY]** The finalization steps below are review-agent only. Implementing agents must not execute this section.
|
||||
|
||||
Compare implementation of each item against source files and verify that output in `Verification Results` matches code.
|
||||
Review completion means the following steps are finished:
|
||||
|
||||
1. Append verdict and `review_rework_count` / `evidence_integrity_failure` routing signals.
|
||||
2. Archive `CODE_REVIEW-cloud-G05.md` → `code_review_cloud_G05_0.log` and `PLAN-cloud-G05.md` → `plan_cloud_G05_0.log`.
|
||||
3. If PASS, write `complete.log` and move active task directory to `agent-task/archive/YYYY/MM/provider_stream_buffer_compat/`. If WARN/FAIL, fully write the next filesystem state required by the code-review skill.
|
||||
4. If PASS and task group is `m-<milestone-slug>`, report completion event metadata. Roadmap state check and `update-roadmap` calls are runtime responsibilities.
|
||||
5. Check applicable `Review-Only Checklist` items at the final `.log` location before reporting.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Item Completion
|
||||
|
||||
| Item | Status |
|
||||
|------|---------|
|
||||
| STREAM_BUFFER-1 — Repeat-policy hold envelope | [ ] |
|
||||
| STREAM_BUFFER-2 — Large semantic-delta exact-wire tunnel regression | [ ] |
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [ ] [STREAM_BUFFER-1] Align both repeat-policy hold requirements to the existing bounded single-event compatibility limit and lock their composed Core envelope with a unit regression.
|
||||
- [ ] [STREAM_BUFFER-2] Add Chat/Responses content/reasoning tunnel regressions for one semantic delta above 4,096 runes with byte-identical successful release.
|
||||
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.
|
||||
|
||||
## Review-Only Checklist
|
||||
|
||||
> **[REVIEW AGENT ONLY]** This checklist is used only by the review agent.
|
||||
> Implementing agents must not modify or check this section.
|
||||
|
||||
- [ ] Append one verdict of `PASS`, `WARN`, or `FAIL` and verified `review_rework_count`, `evidence_integrity_failure` to `Code Review Result`.
|
||||
- [ ] Verify that verdict, `Dimension Assessment`, and Required/Suggested/Nit classifications match.
|
||||
- [ ] Archive active `CODE_REVIEW-*-G??.md` to `code_review_cloud_G05_0.log`.
|
||||
- [ ] Archive active `PLAN-*-G??.md` to `plan_cloud_G05_0.log`.
|
||||
- [ ] Verify that the Agent-Ops managed block in `.gitignore` unignores `agent-task/**/*.md` and `agent-task/**/*.log` and ignores `agent-roadmap/current.md`.
|
||||
- [ ] If PASS, write `complete.log` based on `agent-ops/skills/common/code-review/templates/complete-log-template.md` and leave no active `.md` files.
|
||||
- [ ] If PASS, move active task directory `agent-task/provider_stream_buffer_compat/` to `agent-task/archive/YYYY/MM/provider_stream_buffer_compat/` and update this checklist at the final archive path.
|
||||
- [ ] If PASS and task group is `m-<milestone-slug>`, report completion event metadata for runtime, without modifying roadmap or directly calling `update-roadmap`.
|
||||
- [ ] If PASS for split work, remove empty active parent `agent-task/provider_stream_buffer_compat/` or verify it was kept due to remaining siblings/files.
|
||||
- [ ] If WARN/FAIL, write the next filesystem state matching code-review verdict and do not write `complete.log`.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
_Record any deviations from the plan and the rationale here._
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
_Record key design decisions here._
|
||||
|
||||
## Reviewer Checkpoints
|
||||
|
||||
- Confirm `openAIRepeatHoldMaxBufferRunes` is private, bounded at `1 << 20`, and used by both the repeat rolling guard and automatically registered action sibling, including fallback constructors.
|
||||
- Confirm `hold_evidence_runes`, schema gate default bound, Core implementation, tunnel codec/release queue, config schema, contracts, specs, roadmap, and provider evaluation logic remain unchanged.
|
||||
- Confirm `TestOpenAIRepeatHoldBufferContract` resolves the production repeat pair and checks the composed channel bound, not just an isolated constructor.
|
||||
- Confirm `TestStreamGateConfiguredRepeatGuardLargeTunnelDelta` covers Chat/Responses and content/reasoning with one 5,000-rune event, production registry/Core/tunnel sink, exact original wire, one 200 start, one successful terminal, and zero recovery/error leakage.
|
||||
- Confirm every verification command was run fresh and its actual stdout/stderr is recorded below; investigate any deviation or skipped smoke.
|
||||
|
||||
## Verification Results
|
||||
|
||||
> **[IMPLEMENTING AGENT]** Run each command exactly as written and paste its actual stdout/stderr below the corresponding result field. If a command must change, record the replacement and reason in `Deviations from Plan`.
|
||||
|
||||
### STREAM_BUFFER-1 focused verification
|
||||
|
||||
```bash
|
||||
gofmt -d apps/edge/internal/openai/stream_gate_filters.go apps/edge/internal/openai/stream_gate_filters_test.go
|
||||
go test -count=1 ./apps/edge/internal/openai -run '^Test(OpenAIRepeatHoldBufferContract|OpenAIOutputFilterRegistrations)$'
|
||||
```
|
||||
|
||||
Expected: no formatting diff; the fresh focused tests pass and prove the repeat pair composes to 1 MiB while its evidence window and schema bound remain unchanged.
|
||||
|
||||
Actual stdout/stderr:
|
||||
|
||||
_Fill after implementation._
|
||||
|
||||
### STREAM_BUFFER-2 focused verification
|
||||
|
||||
```bash
|
||||
gofmt -d apps/edge/internal/openai/stream_gate_pipeline_test.go
|
||||
go test -count=1 ./apps/edge/internal/openai -run '^TestStreamGateConfiguredRepeatGuardLargeTunnelDelta$'
|
||||
```
|
||||
|
||||
Expected: no formatting diff; all four fresh subtests return the original wire once with HTTP 200 and a successful terminal.
|
||||
|
||||
Actual stdout/stderr:
|
||||
|
||||
_Fill after implementation._
|
||||
|
||||
### Final verification
|
||||
|
||||
```bash
|
||||
command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT && go env GOMOD
|
||||
make proto
|
||||
git diff --exit-code -- proto/gen/iop
|
||||
gofmt -d apps/edge/internal/openai/stream_gate_filters.go apps/edge/internal/openai/stream_gate_filters_test.go apps/edge/internal/openai/stream_gate_pipeline_test.go
|
||||
go test -count=1 ./apps/edge/internal/openai -run '^Test(OpenAIRepeatHoldBufferContract|StreamGateConfiguredRepeatGuardLargeTunnelDelta)$'
|
||||
go test -race -count=1 ./packages/go/streamgate ./apps/edge/internal/openai ./packages/go/config
|
||||
make test-openai-lemonade
|
||||
IOP_E2E_BIND_TIMEOUT=60 IOP_E2E_PROFILE=mock ./scripts/e2e-smoke.sh
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: the host Go identity remains unchanged; proto generation produces no diff; formatting is clean; fresh focused and race suites pass; the repository-native OpenAI provider tunnel and mock smokes pass; and the final diff has no whitespace errors. No live provider, external runner, or cached Go result is accepted as a substitute for the deterministic large-delta regression.
|
||||
|
||||
Actual stdout/stderr:
|
||||
|
||||
_Fill after implementation._
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
||||
## Section Ownership
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, Overview, Review Agent Instructions | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
|
||||
| Implementation Item Completion (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| Implementation Checklist (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| Review-Only Checklist | Review agent only | Implementing agent must not modify or check this section |
|
||||
| Deviations from Plan, Key Design Decisions | Implementing agent | Replace placeholder text with actual content |
|
||||
| Reviewer Checkpoints | Fixed at stub creation | Pre-filled from plan |
|
||||
| Verification Results (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `Deviations from Plan` entry |
|
||||
| Code Review Result | Review agent appends | Not included in stub |
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
<!-- task=provider_stream_buffer_compat plan=1 tag=STREAM_BUFFER -->
|
||||
|
||||
# Code Review Reference - STREAM_BUFFER
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `Implementation Checklist`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence fields.
|
||||
> Do not ask the user directly, present choices, call user-input tools, create control-plane stop files, or classify the next state.
|
||||
> Finalization (`Code Review Result`, log rename, `complete.log`, archive moves, `Review-Only Checklist`) is review-agent-only, even after compaction/resume.
|
||||
> Follow the ownership table at the bottom of this file for which sections you own.
|
||||
|
||||
## Overview
|
||||
|
||||
date=2026-07-30
|
||||
task=provider_stream_buffer_compat, plan=1, tag=STREAM_BUFFER
|
||||
|
||||
## Archive Evidence Snapshot
|
||||
|
||||
- The user requested a second plan review before implementation. The prior unimplemented draft pair is preserved as `agent-task/provider_stream_buffer_compat/plan_cloud_G05_0.log` and `agent-task/provider_stream_buffer_compat/code_review_cloud_G05_0.log`.
|
||||
- The prior review stub has no verdict, implementation evidence, Required/Suggested/Nit findings, or verification result. This replan retains the diagnosed two-requirement bound fix and production-path regression, corrects the unit from “MiB” to runes, removes a stale prior-task reference, and removes non-diagnostic proto/general smoke commands.
|
||||
- No roadmap state or completion claim carries over.
|
||||
|
||||
## For the Review Agent
|
||||
|
||||
> **[REVIEW AGENT ONLY]** The finalization steps below are review-agent only. Implementing agents must not execute this section.
|
||||
|
||||
Compare implementation of each item against source files and verify that output in `Verification Results` matches code.
|
||||
Review completion means the following steps are finished:
|
||||
|
||||
1. Append verdict and `review_rework_count` / `evidence_integrity_failure` routing signals.
|
||||
2. Archive `CODE_REVIEW-cloud-G05.md` → `code_review_cloud_G05_1.log` and `PLAN-cloud-G05.md` → `plan_cloud_G05_1.log`.
|
||||
3. If PASS, write `complete.log` and move active task directory to `agent-task/archive/YYYY/MM/provider_stream_buffer_compat/`. If WARN/FAIL, fully write the next filesystem state required by the code-review skill.
|
||||
4. If PASS and task group is `m-<milestone-slug>`, report completion event metadata. Roadmap state check and `update-roadmap` calls are runtime responsibilities.
|
||||
5. Check applicable `Review-Only Checklist` items at the final `.log` location before reporting.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Item Completion
|
||||
|
||||
| Item | Status |
|
||||
|------|---------|
|
||||
| STREAM_BUFFER-1 — Repeat-policy hold envelope | [x] |
|
||||
| STREAM_BUFFER-2 — Large semantic-event exact-wire tunnel regression | [x] |
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [x] [STREAM_BUFFER-1] Align both repeat-policy hold requirements to Core's existing 1,048,576-rune ceiling and lock their composed envelope with a unit regression.
|
||||
- [x] [STREAM_BUFFER-2] Add Chat/Responses content/reasoning tunnel regressions for one semantic event above 4,096 runes with byte-identical successful release.
|
||||
- [x] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.
|
||||
|
||||
## Review-Only Checklist
|
||||
|
||||
> **[REVIEW AGENT ONLY]** This checklist is used only by the review agent.
|
||||
> Implementing agents must not modify or check this section.
|
||||
|
||||
- [x] Append one verdict of `PASS`, `WARN`, or `FAIL` and verified `review_rework_count`, `evidence_integrity_failure` to `Code Review Result`.
|
||||
- [x] Verify that verdict, `Dimension Assessment`, and Required/Suggested/Nit classifications match.
|
||||
- [x] Archive active `CODE_REVIEW-*-G??.md` to `code_review_cloud_G05_1.log`.
|
||||
- [x] Archive active `PLAN-*-G??.md` to `plan_cloud_G05_1.log`.
|
||||
- [x] Verify that the Agent-Ops managed block in `.gitignore` unignores `agent-task/**/*.md` and `agent-task/**/*.log` and ignores `agent-roadmap/current.md`.
|
||||
- [x] If PASS, write `complete.log` based on `agent-ops/skills/common/code-review/templates/complete-log-template.md` and leave no active `.md` files.
|
||||
- [x] If PASS, move active task directory `agent-task/provider_stream_buffer_compat/` to `agent-task/archive/YYYY/MM/provider_stream_buffer_compat/` and update this checklist at the final archive path.
|
||||
- [ ] If PASS and task group is `m-<milestone-slug>`, report completion event metadata for runtime, without modifying roadmap or directly calling `update-roadmap`.
|
||||
- [ ] If PASS for split work, remove empty active parent `agent-task/provider_stream_buffer_compat/` or verify it was kept due to remaining siblings/files.
|
||||
- [ ] If WARN/FAIL, write the next filesystem state matching code-review verdict and do not write `complete.log`.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None. All implementation items and verification steps executed exactly as planned.
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
- Defined private constant `openAIRepeatHoldMaxBufferRunes = 1 << 20` (1,048,576 runes) in `apps/edge/internal/openai/stream_gate_filters.go`.
|
||||
- Applied `NewFilterHoldRequirementRollingWithMaxBuffer` and `NewFilterHoldRequirementTerminalGateWithMaxBuffer` with `openAIRepeatHoldMaxBufferRunes` to both `openAIOutputFilterRepeatGuard` and `openAIOutputFilterRepeatActionGuard` (including fallbacks). Left `openAIOutputFilterSchemaGate` on the default constructor.
|
||||
- Added `TestOpenAIRepeatHoldBufferContract` in `apps/edge/internal/openai/stream_gate_filters_test.go` to assert composed channel maximum is 1,048,576 runes, both repeat participant maxima are 1,048,576 runes, rolling evidence window remains 500 runes, and schema default remains 4,096 runes.
|
||||
- Added `TestStreamGateConfiguredRepeatGuardLargeTunnelEvent` in `apps/edge/internal/openai/stream_gate_pipeline_test.go` covering 4 subtests (Chat content, Chat reasoning, Responses output text, Responses reasoning) with 5,000 distinct Korean runes per event, asserting HTTP 200, 1 header commit, successful terminal, exact wire match, and zero recovery dispatches.
|
||||
|
||||
## Reviewer Checkpoints
|
||||
|
||||
- Confirm the change is limited to hold-buffer infrastructure: provider output-filter decisions, repeat evidence window, codec, raw-wire queue, error envelope, config, contract, spec, roadmap, and deployment remain unchanged.
|
||||
- Confirm `openAIRepeatHoldMaxBufferRunes` is private, documented in runes, equal to `1 << 20`, and used by both repeat hold participants including fallback constructors.
|
||||
- Confirm the action sibling's larger tool-fragment allowance is only the necessary channel-level consequence of Core's minimum-bound composition and remains bounded.
|
||||
- Confirm `TestOpenAIRepeatHoldBufferContract` resolves the production repeat pair and checks participant maxima, composed channel maximum, unchanged rolling evidence, and unchanged schema default.
|
||||
- Confirm `TestStreamGateConfiguredRepeatGuardLargeTunnelEvent` covers Chat/Responses × content/reasoning with one distinct 5,000-rune event through the production registry/source/Core/sink boundary, exact original wire, one 200 start, one successful terminal, and zero recovery dispatches.
|
||||
- Confirm no implementation claim is made for `metadata.scheme` requests or events above 1,048,576 runes.
|
||||
- Confirm every listed verification command ran fresh and actual stdout/stderr is recorded below.
|
||||
|
||||
## Verification Results
|
||||
|
||||
> **[IMPLEMENTING AGENT]** Run each command exactly as written and paste its actual stdout/stderr below the corresponding result field. If a command must change, record the replacement and reason in `Deviations from Plan`.
|
||||
|
||||
### STREAM_BUFFER-1 focused verification
|
||||
|
||||
```bash
|
||||
gofmt -d apps/edge/internal/openai/stream_gate_filters.go apps/edge/internal/openai/stream_gate_filters_test.go
|
||||
go test -count=1 ./apps/edge/internal/openai -run '^Test(OpenAIRepeatHoldBufferContract|OpenAIOutputFilterRegistrations)$'
|
||||
```
|
||||
|
||||
Expected: no formatting diff; fresh tests prove the repeat pair composes to 1,048,576 runes while the evidence window and schema bound remain unchanged.
|
||||
|
||||
Actual stdout/stderr:
|
||||
|
||||
```
|
||||
ok iop/apps/edge/internal/openai 0.006s
|
||||
```
|
||||
|
||||
### STREAM_BUFFER-2 focused verification
|
||||
|
||||
```bash
|
||||
gofmt -d apps/edge/internal/openai/stream_gate_pipeline_test.go
|
||||
go test -count=1 ./apps/edge/internal/openai -run '^TestStreamGateConfiguredRepeatGuardLargeTunnelEvent$'
|
||||
```
|
||||
|
||||
Expected: no formatting diff; all four fresh subtests return the original wire once with HTTP 200, one successful terminal, and no recovery dispatch.
|
||||
|
||||
Actual stdout/stderr:
|
||||
|
||||
```
|
||||
ok iop/apps/edge/internal/openai 0.009s
|
||||
```
|
||||
|
||||
### Final verification
|
||||
|
||||
```bash
|
||||
command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT && go env GOMOD
|
||||
gofmt -d apps/edge/internal/openai/stream_gate_filters.go apps/edge/internal/openai/stream_gate_filters_test.go apps/edge/internal/openai/stream_gate_pipeline_test.go
|
||||
go test -count=1 ./apps/edge/internal/openai -run '^Test(OpenAIRepeatHoldBufferContract|StreamGateConfiguredRepeatGuardLargeTunnelEvent)$'
|
||||
go test -race -count=1 ./packages/go/streamgate ./apps/edge/internal/openai ./packages/go/config
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: host Go identity is unchanged; formatting is clean; fresh focused and race suites pass; all four large-event tunnel variants preserve the exact provider wire with one successful terminal; and the diff has no whitespace errors. Proto generation and general mock/provider smoke are excluded because no proto/config/route is changed and those commands do not activate this repeat-policy failure condition.
|
||||
|
||||
Actual stdout/stderr:
|
||||
|
||||
```
|
||||
/config/.local/bin/go
|
||||
/config/opt/go/bin/go
|
||||
go version go1.26.2 linux/arm64
|
||||
/config/opt/go
|
||||
/config/workspace/iop/go.mod
|
||||
ok iop/apps/edge/internal/openai 0.084s
|
||||
ok iop/packages/go/streamgate 1.940s
|
||||
ok iop/apps/edge/internal/openai 8.685s
|
||||
ok iop/packages/go/config 1.268s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section?**
|
||||
> If anything is blank, go back and fill it in before saving this file.
|
||||
> Leave review-agent-only sections unchanged.
|
||||
|
||||
## Section Ownership
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, Overview, Review Agent Instructions | Fixed at stub creation | Implementing agent must not modify or execute these (archive, complete.log, and task-directory archive move are review-agent only) |
|
||||
| Roadmap Targets | Fixed at stub creation from plan when present | Implementing agent must not modify; code-review copies it into `complete.log` as `Roadmap Completion` only on PASS |
|
||||
| Archive Evidence Snapshot | Fixed at stub creation from plan when present | Implementing agent uses it as default prior-loop context; read only the specific archive files cited there when more detail is required |
|
||||
| Implementation Item Completion (item names) | Fixed at stub creation | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| Implementation Checklist (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` → `[x]` only |
|
||||
| Review-Only Checklist | Review agent only | Implementing agent must not modify or check this section |
|
||||
| Deviations from Plan, Key Design Decisions | Implementing agent | Replace placeholder text with actual content |
|
||||
| Reviewer Checkpoints | Fixed at stub creation | Pre-filled from plan |
|
||||
| Verification Results (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `Deviations from Plan` entry |
|
||||
| Code Review Result | Review agent appends | Not included in stub |
|
||||
|
||||
## Code Review Result
|
||||
|
||||
- Overall Verdict: PASS
|
||||
- Dimension Assessment:
|
||||
- Correctness: Pass — both blocking repeat participants use the same bounded 1,048,576-rune envelope, so Core's minimum-bound composition no longer retains the 4,096-rune limit.
|
||||
- Completeness: Pass — both implementation items and their production-path regressions are present with no unresolved checklist gap.
|
||||
- Test Coverage: Pass — the contract test covers participant and composed bounds, and the tunnel regression covers Chat/Responses content/reasoning variants with exact-wire release.
|
||||
- API Contract: Pass — provider status, headers, body bytes, terminal behavior, schema gating, and public error shapes remain unchanged.
|
||||
- Code Quality: Pass — the bound is private, documented in runes, narrowly applied, formatted, and free of debug or dead code.
|
||||
- Implementation Deviation: Pass — the implementation matches the planned files, scope, and exclusions.
|
||||
- Verification Trust: Pass — all focused and final commands were rerun successfully with the declared host Go toolchain; reported behavior and outputs match fresh reviewer evidence.
|
||||
- Findings: None
|
||||
- Routing Signals:
|
||||
- `review_rework_count=0`
|
||||
- `evidence_integrity_failure=false`
|
||||
- Next Step: PASS — write `complete.log`, archive the active plan/review pair, and move the completed task under `agent-task/archive/2026/07/`.
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# Complete - provider_stream_buffer_compat
|
||||
|
||||
## Completed At
|
||||
|
||||
2026-07-30
|
||||
|
||||
## Summary
|
||||
|
||||
Completed in one implemented review after one pre-implementation replan; final verdict PASS.
|
||||
|
||||
## Loop History
|
||||
|
||||
| Plan | Review | Verdict | Notes |
|
||||
|------|--------|---------|-------|
|
||||
| `plan_cloud_G05_0.log` | `code_review_cloud_G05_0.log` | No verdict | Unimplemented draft superseded by the user-requested second plan review. |
|
||||
| `plan_cloud_G05_1.log` | `code_review_cloud_G05_1.log` | PASS | The repeat hold envelope and all four exact-wire large-event tunnel variants passed fresh focused and race verification. |
|
||||
|
||||
## Implementation and Cleanup
|
||||
|
||||
- Added a private 1,048,576-rune maximum buffer for both repeat-policy hold participants while preserving the 500-rune evidence window and the schema gate's 4,096-rune default.
|
||||
- Added a production-registration contract test for the participant and composed bounds.
|
||||
- Added Chat/Responses content/reasoning tunnel regressions for one 5,000-rune semantic event with exact provider-wire release and no recovery dispatch.
|
||||
|
||||
## Final Verification
|
||||
|
||||
- `command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT && go env GOMOD` — PASS; `/config/.local/bin/go` resolves to `/config/opt/go/bin/go`, Go is `go1.26.2 linux/arm64`, `GOROOT=/config/opt/go`, and `GOMOD=/config/workspace/iop/go.mod`.
|
||||
- `gofmt -d apps/edge/internal/openai/stream_gate_filters.go apps/edge/internal/openai/stream_gate_filters_test.go apps/edge/internal/openai/stream_gate_pipeline_test.go` — PASS; no formatting diff.
|
||||
- `go test -count=1 ./apps/edge/internal/openai -run '^Test(OpenAIRepeatHoldBufferContract|OpenAIOutputFilterRegistrations)$'` — PASS; `ok iop/apps/edge/internal/openai`.
|
||||
- `go test -count=1 ./apps/edge/internal/openai -run '^TestStreamGateConfiguredRepeatGuardLargeTunnelEvent$'` — PASS; all four variants passed.
|
||||
- `go test -count=1 ./apps/edge/internal/openai -run '^Test(OpenAIRepeatHoldBufferContract|StreamGateConfiguredRepeatGuardLargeTunnelEvent)$'` — PASS; `ok iop/apps/edge/internal/openai`.
|
||||
- `go test -race -count=1 ./packages/go/streamgate ./apps/edge/internal/openai ./packages/go/config` — PASS; all three packages passed with the race detector.
|
||||
- `git diff --check` — PASS; no whitespace errors.
|
||||
- Repository Edge-Node diagnosis, auxiliary E2E smoke, live-provider smoke, and full-cycle startup were not run because the available flows do not enable the blocking `repeat_guard` configuration and cannot exercise the changed buffer path; the deterministic production registry/source/Core/sink regression is the accepted task oracle.
|
||||
|
||||
## Remaining Nits
|
||||
|
||||
- None
|
||||
|
||||
## Follow-up Work
|
||||
|
||||
- None
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
<!-- task=provider_stream_buffer_compat plan=0 tag=STREAM_BUFFER -->
|
||||
|
||||
# Provider stream single-event buffer compatibility hardening
|
||||
|
||||
## For the Implementing Agent
|
||||
|
||||
Filling the implementation-owned sections in `CODE_REVIEW-*-G??.md` is mandatory. Run every verification command, paste actual notes and stdout/stderr into the active review file, keep the active PLAN/CODE_REVIEW files in place, and report ready for review; only the code-review skill may finalize or archive the task. If blocked, record only the exact blocker, attempted commands/output, and resume conditions in implementation-owned evidence fields. Do not ask the user, call user-input tools, create control-plane stop files, classify the next state, archive logs, or write `complete.log`.
|
||||
|
||||
## Background
|
||||
|
||||
The configured repeat policy currently inherits Core's 4,096-rune default hard buffer for both its rolling text guard and terminal action sibling. A provider tunnel SSE frame can decode into one valid semantic content or reasoning event larger than that default, causing Core to emit `buffer_overflow` before repeat evaluation and Edge to return a 502 `provider_tunnel_error` even though the provider response is valid. This patch aligns only the repeat policy's hold envelope with Core's existing bounded 1 MiB maximum and preserves exact provider wire release.
|
||||
|
||||
## Analysis
|
||||
|
||||
### Files Read
|
||||
|
||||
- Workflow and rules: `AGENTS.md`, `agent-ops/rules/project/rules.md`, `agent-ops/rules/private/rules.md`, `agent-ops/rules/common/rules-roadmap.md`, `agent-ops/rules/common/rules-agent-spec.md`, `agent-ops/rules/project/domain/edge/rules.md`, `agent-ops/rules/project/domain/platform-common/rules.md`, `agent-ops/rules/project/domain/testing/rules.md`, `agent-ops/skills/common/router.md`, `agent-ops/skills/common/plan/SKILL.md`, `agent-ops/skills/common/finalize-task-routing/SKILL.md`, and `agent-ops/skills/common/plan/templates/review-stub-template.md`.
|
||||
- Verification profiles: `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/platform-common-smoke.md`, and `agent-test/local/testing-smoke.md`.
|
||||
- Contracts and current specs: `agent-contract/index.md`, `agent-contract/outer/openai-compatible-api.md`, `agent-contract/inner/edge-node-runtime-wire.md`, `agent-spec/index.md`, `agent-spec/runtime/stream-evidence-gate.md`, and `agent-spec/input/openai-compatible-surface.md`.
|
||||
- Edge source: `apps/edge/internal/openai/stream_gate_filters.go`, `apps/edge/internal/openai/stream_gate_policy.go`, `apps/edge/internal/openai/stream_gate_tunnel_codec.go`, `apps/edge/internal/openai/stream_gate_release_sink.go`, and the relevant runtime assembly/error paths in `apps/edge/internal/openai/stream_gate_runtime.go`.
|
||||
- Core source: `packages/go/streamgate/evidence_tail.go`, plus the relevant plan installation and overflow mapping paths in `packages/go/streamgate/runtime.go` and filter resolution accessors in `packages/go/streamgate/filter_registry.go`.
|
||||
- Tests: `apps/edge/internal/openai/stream_gate_filters_test.go`, `apps/edge/internal/openai/stream_gate_policy_test.go`, `apps/edge/internal/openai/stream_gate_pipeline_test.go`, and `packages/go/streamgate/stream_release_test.go`.
|
||||
- Active task state: `agent-task/m-openai-compatible-output-validation-filters/02+01_repeat_guard/PLAN-cloud-G10.md`; it remains a separate Milestone verification task and is not reused or modified by this patch.
|
||||
|
||||
### SDD Criteria
|
||||
|
||||
Not applicable. This is a first-pass, non-roadmap compatibility patch and must not check or modify any Milestone Task or SDD state.
|
||||
|
||||
### Verification Context
|
||||
|
||||
- No `verification_context` handoff was supplied. The repository-native fallback is the current OpenAI contract/spec, Core hold implementation, production Edge tunnel codec/runtime/release sink, existing exact-wire tests, local test rules, and Makefile targets.
|
||||
- Local preflight on 2026-07-30: branch `feature/openai-compatible-output-validation-filters`, HEAD `213eee4e28dfa69ff1e412faf23978ee9a9a3b9f`, clean worktree; Go resolves from `/config/.local/bin/go` to `/config/opt/go/bin/go`, version `go1.26.2 linux/arm64`, `GOROOT=/config/opt/go`, `GOMOD=/config/workspace/iop/go.mod`.
|
||||
- Existing focused harness proof passed uncached: `go test -count=1 ./apps/edge/internal/openai -run 'Test(RepeatGuardIdleDoesNotRelease|OpenAIOutputFilterRegistrations|OpenAITunnelCodecTerminalWire|StreamGateConfiguredRepeatActionSplitLifecycle)$'`.
|
||||
- `make -n proto` and `make -n test-openai-lemonade` confirm both repository-native commands resolve in this checkout. `gofmt -d` is clean for the three planned code/test files.
|
||||
- No external runner, provider, shared runtime, dev deployment, credential, or network endpoint is required. The decisive oracle is an in-process production registry + tunnel event source + Core runtime + tunnel release sink driven by deterministic provider frames.
|
||||
- Fresh `-count=1` and `-race -count=1` results are required; cached Go test output is not acceptable. Confidence is high because the failure and success paths share the exact production codec/Core/sink boundary.
|
||||
|
||||
### Test Coverage Gaps
|
||||
|
||||
- Existing Core tests cover explicit `max_buffer_runes`, Unicode rune counting, overflow, discard, and single terminal behavior, but no Edge test asserts the repeat text guard and its automatically registered action sibling compose to a bound above 4,096.
|
||||
- Existing tunnel tests cover Chat/Responses parsing, content/reasoning event shapes, terminal framing, split tool identity, and byte-identical release only with small semantic deltas.
|
||||
- No current test drives one content or reasoning semantic event larger than 4,096 runes through the configured repeat policy and full tunnel runtime. `STREAM_BUFFER-1` closes the policy-composition gap; `STREAM_BUFFER-2` closes the transport regression gap.
|
||||
|
||||
### Symbol References
|
||||
|
||||
None. No symbol is renamed or removed, and no new public API or dependency is introduced.
|
||||
|
||||
### Split Judgment
|
||||
|
||||
Keep one compact plan. The indivisible invariant is that a repeat-enabled provider tunnel must accept one valid coalesced semantic event within the existing 1 MiB Core bound and release the original provider SSE bytes exactly once; the hold configuration and runtime regression cannot independently prove that invariant.
|
||||
|
||||
### Scope Rationale
|
||||
|
||||
- Change only the repeat policy's rolling text/reasoning hold and terminal action sibling hold. Both must use the same bound because Core composes blocking requirements on one channel using the minimum positive `max_buffer_runes`.
|
||||
- Preserve `hold_evidence_runes` as the repeat evaluation window. The new hard bound is storage/transport compatibility, not a larger repeat-detection window.
|
||||
- Preserve the schema gate's default 4,096-rune terminal bound. Requests with `metadata.scheme` have a distinct full-output validation policy and are not part of this communication patch.
|
||||
- Do not change `packages/go/streamgate`, codec framing/parsing, release queue semantics, 502 error serialization, config schema, contracts, specs, roadmap, deployment, or provider output filtering/evaluation logic.
|
||||
- Do not split or reserialize provider frames. One raw frame is attached to the first semantic event; semantic fragmentation would require group-aware release semantics to avoid releasing the full raw frame before every derived event is evaluated.
|
||||
- Values above 1 MiB remain intentionally bounded by Core and may still fail closed.
|
||||
|
||||
### Final Routing
|
||||
|
||||
- `status=routed`; `evaluation_mode=first-pass`; `finalizer=finalize-task-policy.sh`; `finalizer_mode=pair`.
|
||||
- Build closures: scope/context/verification/evidence/ownership/decision closed. Grade scores `1/1/1/1/1`, base basis `local-fit`, final route basis `risk-boundary`, lane `cloud`, grade `G05`, canonical file `PLAN-cloud-G05.md`.
|
||||
- Review closures: scope/context/verification/evidence/ownership/decision closed. Grade scores `1/1/1/1/1`, route `official-review`, lane `cloud`, grade `G05`, adapter `codex`, model `gpt-5.6-sol`, reasoning effort `xhigh`, canonical file `CODE_REVIEW-cloud-G05.md`.
|
||||
- `large_indivisible_context=false`.
|
||||
- Positive loop risks: `temporal_state`, `boundary_contract`, `structured_interpretation`, and `variant_product`; count `4`.
|
||||
- Recovery signals: `review_rework_count=0`, `evidence_integrity_failure=false`; risk boundary matched, recovery boundary did not.
|
||||
- Capability gap: none.
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [ ] [STREAM_BUFFER-1] Align both repeat-policy hold requirements to the existing bounded single-event compatibility limit and lock their composed Core envelope with a unit regression.
|
||||
- [ ] [STREAM_BUFFER-2] Add Chat/Responses content/reasoning tunnel regressions for one semantic delta above 4,096 runes with byte-identical successful release.
|
||||
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.
|
||||
|
||||
### [STREAM_BUFFER-1] Repeat-policy hold envelope
|
||||
|
||||
#### Problem
|
||||
|
||||
`apps/edge/internal/openai/stream_gate_filters.go:131-173` builds the repeat rolling guard and its terminal action sibling with Core's default constructors. Both therefore carry the 4,096-rune default from `packages/go/streamgate/evidence_tail.go:74-81,122-174`. `packages/go/streamgate/evidence_tail.go:480-483` composes same-channel blocking requirements using the minimum positive bound, so changing only the rolling guard would leave the action sibling's 4,096 limit effective.
|
||||
|
||||
Before:
|
||||
|
||||
```go
|
||||
// apps/edge/internal/openai/stream_gate_filters.go:131-173
|
||||
func (f *openAIOutputFilter) HoldRequirement(streamgate.FilterContext) streamgate.FilterHoldRequirement {
|
||||
switch f.kind {
|
||||
case openAIOutputFilterRepeatGuard:
|
||||
req, err := streamgate.NewFilterHoldRequirementRolling(
|
||||
f.channel,
|
||||
[]streamgate.EventKind{
|
||||
streamgate.EventKindTextDelta,
|
||||
streamgate.EventKindReasoningDelta,
|
||||
},
|
||||
f.holdRunes,
|
||||
)
|
||||
// ...
|
||||
case openAIOutputFilterRepeatActionGuard:
|
||||
req, err := streamgate.NewFilterHoldRequirementTerminalGate(
|
||||
f.channel,
|
||||
[]streamgate.EventKind{
|
||||
streamgate.EventKindToolCallFragment,
|
||||
streamgate.EventKindTerminal,
|
||||
},
|
||||
streamgate.EventKindTerminal,
|
||||
)
|
||||
// ...
|
||||
case openAIOutputFilterSchemaGate:
|
||||
req, err := streamgate.NewFilterHoldRequirementTerminalGate(
|
||||
// ...
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Solution
|
||||
|
||||
- Add one unexported Edge constant, `openAIRepeatHoldMaxBufferRunes = 1 << 20`, documented as the bounded allowance for a coalesced provider semantic event, not the rolling evidence window.
|
||||
- Use `NewFilterHoldRequirementRollingWithMaxBuffer` for `openAIOutputFilterRepeatGuard` and `NewFilterHoldRequirementTerminalGateWithMaxBuffer` for `openAIOutputFilterRepeatActionGuard`, including their defensive fallback calls.
|
||||
- Leave `openAIOutputFilterSchemaGate` on `NewFilterHoldRequirementTerminalGate` so its default bound does not silently change.
|
||||
- Extend the filter registration test surface with `TestOpenAIRepeatHoldBufferContract`: resolve the production repeat registration without a schema, compile `EvidencePlan`, and assert the composed channel maximum is `openAIRepeatHoldMaxBufferRunes`, rolling evidence remains the configured value, both repeat participants carry the same maximum, and a standalone schema requirement remains 4,096.
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
// A provider may coalesce one valid content/reasoning delta above Core's
|
||||
// default hold size. This remains Core-bounded and does not widen evidence.
|
||||
const openAIRepeatHoldMaxBufferRunes = 1 << 20
|
||||
|
||||
case openAIOutputFilterRepeatGuard:
|
||||
req, err := streamgate.NewFilterHoldRequirementRollingWithMaxBuffer(
|
||||
f.channel,
|
||||
[]streamgate.EventKind{
|
||||
streamgate.EventKindTextDelta,
|
||||
streamgate.EventKindReasoningDelta,
|
||||
},
|
||||
f.holdRunes,
|
||||
openAIRepeatHoldMaxBufferRunes,
|
||||
)
|
||||
// fallback uses the same explicit bound
|
||||
return req
|
||||
case openAIOutputFilterRepeatActionGuard:
|
||||
req, err := streamgate.NewFilterHoldRequirementTerminalGateWithMaxBuffer(
|
||||
f.channel,
|
||||
[]streamgate.EventKind{
|
||||
streamgate.EventKindToolCallFragment,
|
||||
streamgate.EventKindTerminal,
|
||||
},
|
||||
streamgate.EventKindTerminal,
|
||||
openAIRepeatHoldMaxBufferRunes,
|
||||
)
|
||||
// fallback uses the same explicit bound
|
||||
return req
|
||||
case openAIOutputFilterSchemaGate:
|
||||
// Keep the existing default constructor and bound.
|
||||
```
|
||||
|
||||
#### Modified Files and Checklist
|
||||
|
||||
- [ ] `apps/edge/internal/openai/stream_gate_filters.go` — add the private compatibility bound and apply it to both repeat hold requirements only.
|
||||
- [ ] `apps/edge/internal/openai/stream_gate_filters_test.go` — add `TestOpenAIRepeatHoldBufferContract` covering participant, composed-plan, evidence-window, and schema non-regression assertions.
|
||||
- [ ] `agent-task/provider_stream_buffer_compat/CODE_REVIEW-cloud-G05.md` — record implementation decisions and actual item verification output.
|
||||
|
||||
#### Test Strategy
|
||||
|
||||
Write a regression test because this is a bug fix. `TestOpenAIRepeatHoldBufferContract` must use production registration/resolution rather than only direct constructor calls so omission of the automatically registered action sibling fails the test. It must assert exact rune limits and preserve the schema boundary explicitly.
|
||||
|
||||
#### Verification
|
||||
|
||||
```bash
|
||||
gofmt -d apps/edge/internal/openai/stream_gate_filters.go apps/edge/internal/openai/stream_gate_filters_test.go
|
||||
go test -count=1 ./apps/edge/internal/openai -run '^Test(OpenAIRepeatHoldBufferContract|OpenAIOutputFilterRegistrations)$'
|
||||
```
|
||||
|
||||
Expected: no formatting diff; the fresh focused tests pass and prove the repeat pair composes to 1 MiB while its evidence window and schema bound remain unchanged.
|
||||
|
||||
### [STREAM_BUFFER-2] Large semantic-delta exact-wire tunnel regression
|
||||
|
||||
#### Problem
|
||||
|
||||
`apps/edge/internal/openai/stream_gate_tunnel_codec.go:239-286,323-408,444-556` parses a complete SSE frame into one content or reasoning event and retains the original frame for release. `packages/go/streamgate/evidence_tail.go:1569-1582` checks the event's rune count against the composed hold bound before evaluation. Existing exact-wire fixtures at `apps/edge/internal/openai/stream_gate_pipeline_test.go:375-443` use only small deltas, so they do not reproduce the valid greater-than-4,096 event that currently terminates as a 502.
|
||||
|
||||
Before:
|
||||
|
||||
```go
|
||||
// apps/edge/internal/openai/stream_gate_pipeline_test.go:375-443
|
||||
func TestOpenAITunnelCodecTerminalWire(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
frames [][]byte
|
||||
}{
|
||||
{
|
||||
name: "chat finish then done",
|
||||
endpoint: openAIRebuildEndpointChat,
|
||||
frames: [][]byte{
|
||||
[]byte("data: {\"choices\":[{\"delta\":{\"content\":\"answer\"}}]}\n\n"),
|
||||
// ...
|
||||
},
|
||||
},
|
||||
// Responses and transport-end variants also use small payloads.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Solution
|
||||
|
||||
- Add `TestStreamGateConfiguredRepeatGuardLargeTunnelDelta` beside the production tunnel lifecycle tests.
|
||||
- Use a table with four cases: Chat content, Chat reasoning, Responses output text, and Responses reasoning. Each case must encode one SSE JSON frame containing 5,000 distinct Korean runes, followed by the endpoint terminal wire.
|
||||
- Configure only blocking `repeat_guard` with the normal 500-rune evidence window and no schema metadata. Build the production registry, real tunnel event source/Core runtime, and real tunnel release sink over buffered `ProviderTunnelFrame` fixtures.
|
||||
- For every case, require runtime success, one HTTP 200 start, one successful terminal, zero recovery dispatches, no `provider_tunnel_error`/`buffer_overflow`, and caller body bytes exactly equal to the concatenated provider frames.
|
||||
- Keep the provider frame whole. Do not add codec fragmentation or a replacement serializer.
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
func TestStreamGateConfiguredRepeatGuardLargeTunnelDelta(t *testing.T) {
|
||||
payload := uniqueKoreanRunes(5000)
|
||||
tests := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
frame []byte
|
||||
}{
|
||||
{name: "chat content", endpoint: openAIRebuildEndpointChat, frame: chatContentFrame(payload)},
|
||||
{name: "chat reasoning", endpoint: openAIRebuildEndpointChat, frame: chatReasoningFrame(payload)},
|
||||
{name: "responses output text", endpoint: openAIRebuildEndpointResponses, frame: responsesTextFrame(payload)},
|
||||
{name: "responses reasoning", endpoint: openAIRebuildEndpointResponses, frame: responsesReasoningFrame(payload)},
|
||||
}
|
||||
// Each case runs production registry -> endpoint codec -> Core -> tunnel sink
|
||||
// and compares the caller body with the original semantic + terminal frames.
|
||||
}
|
||||
```
|
||||
|
||||
Use local fixture construction with `encoding/json` or existing imports; do not add a package dependency. Helper names in the outline are illustrative and may be replaced by a compact local frame builder within the same test.
|
||||
|
||||
#### Modified Files and Checklist
|
||||
|
||||
- [ ] `apps/edge/internal/openai/stream_gate_pipeline_test.go` — add the four-case production runtime regression and exact-wire assertions.
|
||||
- [ ] `agent-task/provider_stream_buffer_compat/CODE_REVIEW-cloud-G05.md` — record the actual regression output and any justified deviation.
|
||||
|
||||
#### Test Strategy
|
||||
|
||||
Write the integration regression in the existing pipeline test file. The 5,000-rune distinct payload is above the old default and below the new bound, avoids accidentally triggering repeat detection, exercises UTF-8 rune rather than byte accounting, and covers both endpoint and semantic-kind axes.
|
||||
|
||||
#### Verification
|
||||
|
||||
```bash
|
||||
gofmt -d apps/edge/internal/openai/stream_gate_pipeline_test.go
|
||||
go test -count=1 ./apps/edge/internal/openai -run '^TestStreamGateConfiguredRepeatGuardLargeTunnelDelta$'
|
||||
```
|
||||
|
||||
Expected: no formatting diff; all four fresh subtests return the original wire once with HTTP 200 and a successful terminal.
|
||||
|
||||
## Modified Files Summary
|
||||
|
||||
| File | Items |
|
||||
|------|-------|
|
||||
| `apps/edge/internal/openai/stream_gate_filters.go` | STREAM_BUFFER-1 |
|
||||
| `apps/edge/internal/openai/stream_gate_filters_test.go` | STREAM_BUFFER-1 |
|
||||
| `apps/edge/internal/openai/stream_gate_pipeline_test.go` | STREAM_BUFFER-2 |
|
||||
| `agent-task/provider_stream_buffer_compat/CODE_REVIEW-cloud-G05.md` | STREAM_BUFFER-1, STREAM_BUFFER-2 implementation evidence |
|
||||
|
||||
## Dependencies and Execution Order
|
||||
|
||||
1. Implement `STREAM_BUFFER-1` so the production registry resolves the corrected composed hold bound.
|
||||
2. Implement `STREAM_BUFFER-2` against that production registry and retain exact provider wire.
|
||||
3. Run the focused commands, then the complete final verification, and fill the active review evidence.
|
||||
|
||||
## Final Verification
|
||||
|
||||
```bash
|
||||
command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT && go env GOMOD
|
||||
make proto
|
||||
git diff --exit-code -- proto/gen/iop
|
||||
gofmt -d apps/edge/internal/openai/stream_gate_filters.go apps/edge/internal/openai/stream_gate_filters_test.go apps/edge/internal/openai/stream_gate_pipeline_test.go
|
||||
go test -count=1 ./apps/edge/internal/openai -run '^Test(OpenAIRepeatHoldBufferContract|StreamGateConfiguredRepeatGuardLargeTunnelDelta)$'
|
||||
go test -race -count=1 ./packages/go/streamgate ./apps/edge/internal/openai ./packages/go/config
|
||||
make test-openai-lemonade
|
||||
IOP_E2E_BIND_TIMEOUT=60 IOP_E2E_PROFILE=mock ./scripts/e2e-smoke.sh
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: the host Go identity remains unchanged; proto generation produces no diff; formatting is clean; fresh focused and race suites pass; the repository-native OpenAI provider tunnel and mock smokes pass; and the final diff has no whitespace errors. No live provider, external runner, or cached Go result is accepted as a substitute for the deterministic large-delta regression.
|
||||
|
||||
After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.
|
||||
|
|
@ -0,0 +1,276 @@
|
|||
<!-- task=provider_stream_buffer_compat plan=1 tag=STREAM_BUFFER -->
|
||||
|
||||
# Provider stream single-event buffer compatibility hardening
|
||||
|
||||
## For the Implementing Agent
|
||||
|
||||
Filling the implementation-owned sections in `CODE_REVIEW-*-G??.md` is mandatory. Run every verification command, paste actual notes and stdout/stderr into the active review file, keep the active PLAN/CODE_REVIEW files in place, and report ready for review; only the code-review skill may finalize or archive the task. If blocked, record only the exact blocker, attempted commands/output, and resume conditions in implementation-owned evidence fields. Do not ask the user, call user-input tools, create control-plane stop files, classify the next state, archive logs, or write `complete.log`.
|
||||
|
||||
## Background
|
||||
|
||||
The current branch's configured repeat policy inherits Core's 4,096-rune default hard buffer for both its rolling text guard and terminal action sibling. A provider tunnel SSE frame can decode into one valid content or reasoning event larger than that default, causing Core to emit `buffer_overflow` before repeat evaluation and Edge to return a 502 `provider_tunnel_error`. This patch changes only the repeat policy's infrastructure hold envelope to Core's existing 1,048,576-rune ceiling; it does not change provider output-filter decisions or provider wire bytes.
|
||||
|
||||
## Archive Evidence Snapshot
|
||||
|
||||
- The user requested a second plan review before implementation. The prior unimplemented draft pair is preserved as `agent-task/provider_stream_buffer_compat/plan_cloud_G05_0.log` and `agent-task/provider_stream_buffer_compat/code_review_cloud_G05_0.log`.
|
||||
- The prior review stub has no verdict, implementation evidence, Required/Suggested/Nit findings, or verification result. This replan retains the diagnosed two-requirement bound fix and production-path regression, corrects the unit from “MiB” to runes, removes a stale prior-task reference, and removes non-diagnostic proto/general smoke commands.
|
||||
- No roadmap state or completion claim carries over.
|
||||
|
||||
## Analysis
|
||||
|
||||
### Files Read
|
||||
|
||||
- Workflow and rules: `AGENTS.md`, `agent-ops/rules/project/rules.md`, `agent-ops/rules/private/rules.md`, `agent-ops/rules/common/rules-roadmap.md`, `agent-ops/rules/common/rules-agent-spec.md`, `agent-ops/rules/project/domain/edge/rules.md`, `agent-ops/rules/project/domain/platform-common/rules.md`, `agent-ops/rules/project/domain/testing/rules.md`, `agent-ops/skills/common/router.md`, `agent-ops/skills/common/plan/SKILL.md`, `agent-ops/skills/common/finalize-task-routing/SKILL.md`, and `agent-ops/skills/common/plan/templates/review-stub-template.md`.
|
||||
- Verification profiles: `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, `agent-test/local/platform-common-smoke.md`, and `agent-test/local/testing-smoke.md`.
|
||||
- Contracts and current specs: `agent-contract/index.md`, `agent-contract/outer/openai-compatible-api.md`, `agent-contract/inner/edge-node-runtime-wire.md`, `agent-spec/index.md`, `agent-spec/runtime/stream-evidence-gate.md`, and `agent-spec/input/openai-compatible-surface.md`.
|
||||
- Edge source: `apps/edge/internal/openai/stream_gate_filters.go`, `apps/edge/internal/openai/stream_gate_policy.go`, `apps/edge/internal/openai/stream_gate_tunnel_codec.go`, `apps/edge/internal/openai/stream_gate_release_sink.go`, and `apps/edge/internal/openai/stream_gate_runtime.go`.
|
||||
- Core source: `packages/go/streamgate/evidence_tail.go`, `packages/go/streamgate/runtime.go`, and `packages/go/streamgate/filter_registry.go`.
|
||||
- Tests: `apps/edge/internal/openai/stream_gate_filters_test.go`, `apps/edge/internal/openai/stream_gate_policy_test.go`, `apps/edge/internal/openai/stream_gate_pipeline_test.go`, and `packages/go/streamgate/stream_release_test.go`.
|
||||
- Prior task-local draft: `agent-task/provider_stream_buffer_compat/plan_cloud_G05_0.log` and `agent-task/provider_stream_buffer_compat/code_review_cloud_G05_0.log`.
|
||||
|
||||
### SDD Criteria
|
||||
|
||||
Not applicable. This is a non-roadmap compatibility patch and must not check or modify any Milestone Task or SDD state.
|
||||
|
||||
### Verification Context
|
||||
|
||||
- No `verification_context` handoff was supplied. Repository-native evidence is the current OpenAI contract/spec, Core hold implementation, production Edge tunnel codec/runtime/release sink, existing exact-wire tests, and local test rules.
|
||||
- Local preflight on 2026-07-30: branch `feature/openai-compatible-output-validation-filters`, HEAD `213eee4e28dfa69ff1e412faf23978ee9a9a3b9f`; Go resolves from `/config/.local/bin/go` to `/config/opt/go/bin/go`, version `go1.26.2 linux/arm64`, `GOROOT=/config/opt/go`, `GOMOD=/config/workspace/iop/go.mod`.
|
||||
- The worktree has user-owned deletions under `agent-task/m-openai-compatible-output-validation-filters/02+01_repeat_guard/` and this task's plan artifacts. Preserve those deletions; no planned source file has a diff. No dispatcher process is running.
|
||||
- Existing focused harness proof passed uncached: `go test -count=1 ./apps/edge/internal/openai -run 'Test(RepeatGuardIdleDoesNotRelease|OpenAIOutputFilterRegistrations|OpenAITunnelCodecTerminalWire|StreamGateConfiguredRepeatActionSplitLifecycle)$'`.
|
||||
- No external runner, provider, shared runtime, dev deployment, credential, network endpoint, protobuf generation, or config migration is required. The decisive oracle is an in-process production registry + tunnel event source + Core runtime + tunnel release sink driven by deterministic provider frames.
|
||||
- General mock/provider smoke commands do not enable `stream_evidence_gate` with blocking `repeat_guard` and cannot distinguish the old 4,096-rune failure from the fix. They are not acceptance evidence for this patch; the production-path regression below is.
|
||||
- Fresh `-count=1` and `-race -count=1` results are required; cached Go test output is not acceptable. Confidence is high because the regression crosses the exact production codec/Core/sink boundary.
|
||||
|
||||
### Test Coverage Gaps
|
||||
|
||||
- Core tests cover explicit `max_buffer_runes`, Unicode rune counting, overflow, discard, and terminal behavior, but no Edge test locks the bound produced by the repeat text guard plus its automatically registered action sibling.
|
||||
- Existing tunnel tests cover Chat/Responses event shapes, terminal framing, split tool identity, and byte-identical release only with small semantic deltas.
|
||||
- No current test drives one content or reasoning event larger than 4,096 runes through the configured repeat policy and complete tunnel runtime. `STREAM_BUFFER-1` closes the policy-composition gap; `STREAM_BUFFER-2` closes the observed transport regression gap.
|
||||
|
||||
### Symbol References
|
||||
|
||||
None. No symbol is renamed or removed, and no public API or dependency is introduced.
|
||||
|
||||
### Split Judgment
|
||||
|
||||
Keep one compact plan. The indivisible invariant is that a repeat-enabled provider tunnel accepts one valid coalesced semantic event within Core's existing ceiling and releases the original SSE bytes exactly once; the hold configuration and runtime regression cannot independently prove that invariant.
|
||||
|
||||
### Scope Rationale
|
||||
|
||||
- Change only the repeat policy's rolling text/reasoning hold and terminal action sibling hold. Both need the same bound because Core composes blocking requirements on one channel using the minimum positive `max_buffer_runes`.
|
||||
- Preserve `hold_evidence_runes` as the repeat evaluation window. The new hard bound is buffer compatibility, not a larger repeat-detection window.
|
||||
- Raising the action sibling's bound also raises the channel-level allowance for pending tool-call fragments. This is an unavoidable consequence of the current channel-level Core composition, remains bounded at 1,048,576 runes, and does not change action evaluation or release rules.
|
||||
- Preserve the schema gate's default 4,096-rune terminal bound. A request with `metadata.scheme` is a distinct full-output validation boundary and is not claimed fixed by this communication patch.
|
||||
- Do not change `packages/go/streamgate`, codec framing/parsing, release queue semantics, 502 serialization, config schema, contracts, specs, roadmap, deployment, or provider output-filter evaluation behavior.
|
||||
- Do not split or reserialize provider frames. Core already buffers and releases a normalized event as one entry; codec fragmentation would require group-aware release semantics and is outside this fix.
|
||||
- Events above 1,048,576 runes remain intentionally fail-closed.
|
||||
|
||||
### Final Routing
|
||||
|
||||
- `status=routed`; `evaluation_mode=isolated-reassessment`; `finalizer=finalize-task-policy.sh`; `finalizer_mode=pair`.
|
||||
- Build closures: scope/context/verification/evidence/ownership/decision closed. Grade scores `1/1/1/1/1`, base basis `local-fit`, final route basis `risk-boundary`, lane `cloud`, grade `G05`, canonical file `PLAN-cloud-G05.md`.
|
||||
- Review closures: scope/context/verification/evidence/ownership/decision closed. Grade scores `1/1/1/1/1`, route `official-review`, lane `cloud`, grade `G05`, adapter `codex`, model `gpt-5.6-sol`, reasoning effort `xhigh`, canonical file `CODE_REVIEW-cloud-G05.md`.
|
||||
- `large_indivisible_context=false`.
|
||||
- Positive loop risks: `temporal_state`, `boundary_contract`, `structured_interpretation`, and `variant_product`; count `4`.
|
||||
- Recovery signals: `review_rework_count=0`, `evidence_integrity_failure=false`; risk boundary matched, recovery boundary did not.
|
||||
- Capability gap: none.
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [ ] [STREAM_BUFFER-1] Align both repeat-policy hold requirements to Core's existing 1,048,576-rune ceiling and lock their composed envelope with a unit regression.
|
||||
- [ ] [STREAM_BUFFER-2] Add Chat/Responses content/reasoning tunnel regressions for one semantic event above 4,096 runes with byte-identical successful release.
|
||||
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.
|
||||
|
||||
### [STREAM_BUFFER-1] Repeat-policy hold envelope
|
||||
|
||||
#### Problem
|
||||
|
||||
`apps/edge/internal/openai/stream_gate_filters.go:131-173` builds the repeat rolling guard and terminal action sibling with default constructors. Both inherit the 4,096-rune default from `packages/go/streamgate/evidence_tail.go:74-81,157-197`. `packages/go/streamgate/evidence_tail.go:448-483` composes same-channel blocking requirements using the minimum positive bound, so changing only one repeat participant leaves 4,096 effective.
|
||||
|
||||
Before:
|
||||
|
||||
```go
|
||||
// apps/edge/internal/openai/stream_gate_filters.go:131-173
|
||||
case openAIOutputFilterRepeatGuard:
|
||||
req, err := streamgate.NewFilterHoldRequirementRolling(
|
||||
f.channel,
|
||||
[]streamgate.EventKind{
|
||||
streamgate.EventKindTextDelta,
|
||||
streamgate.EventKindReasoningDelta,
|
||||
},
|
||||
f.holdRunes,
|
||||
)
|
||||
// ...
|
||||
case openAIOutputFilterRepeatActionGuard:
|
||||
req, err := streamgate.NewFilterHoldRequirementTerminalGate(
|
||||
f.channel,
|
||||
[]streamgate.EventKind{
|
||||
streamgate.EventKindToolCallFragment,
|
||||
streamgate.EventKindTerminal,
|
||||
},
|
||||
streamgate.EventKindTerminal,
|
||||
)
|
||||
```
|
||||
|
||||
#### Solution
|
||||
|
||||
- Add private Edge constant `openAIRepeatHoldMaxBufferRunes = 1 << 20`, documented as a coalesced semantic-event allowance measured in runes, not bytes and not the rolling evidence window.
|
||||
- Use `NewFilterHoldRequirementRollingWithMaxBuffer` for `openAIOutputFilterRepeatGuard` and `NewFilterHoldRequirementTerminalGateWithMaxBuffer` for `openAIOutputFilterRepeatActionGuard`, including their defensive fallback calls.
|
||||
- Leave `openAIOutputFilterSchemaGate` on the default constructor.
|
||||
- Add `TestOpenAIRepeatHoldBufferContract`: resolve the production repeat registration without schema, compile `streamgate.EvidencePlan`, and assert the composed channel maximum and both repeat participant maxima are 1,048,576 runes, the rolling evidence value remains configured, and a standalone schema requirement remains 4,096.
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
// A provider may coalesce one valid content/reasoning delta above Core's
|
||||
// default hold size. This bound is measured in runes and does not widen evidence.
|
||||
const openAIRepeatHoldMaxBufferRunes = 1 << 20
|
||||
|
||||
case openAIOutputFilterRepeatGuard:
|
||||
req, err := streamgate.NewFilterHoldRequirementRollingWithMaxBuffer(
|
||||
f.channel,
|
||||
[]streamgate.EventKind{
|
||||
streamgate.EventKindTextDelta,
|
||||
streamgate.EventKindReasoningDelta,
|
||||
},
|
||||
f.holdRunes,
|
||||
openAIRepeatHoldMaxBufferRunes,
|
||||
)
|
||||
// fallback uses the same explicit bound
|
||||
return req
|
||||
case openAIOutputFilterRepeatActionGuard:
|
||||
req, err := streamgate.NewFilterHoldRequirementTerminalGateWithMaxBuffer(
|
||||
f.channel,
|
||||
[]streamgate.EventKind{
|
||||
streamgate.EventKindToolCallFragment,
|
||||
streamgate.EventKindTerminal,
|
||||
},
|
||||
streamgate.EventKindTerminal,
|
||||
openAIRepeatHoldMaxBufferRunes,
|
||||
)
|
||||
// fallback uses the same explicit bound
|
||||
return req
|
||||
case openAIOutputFilterSchemaGate:
|
||||
// Keep the existing default constructor and bound.
|
||||
```
|
||||
|
||||
#### Modified Files and Checklist
|
||||
|
||||
- [ ] `apps/edge/internal/openai/stream_gate_filters.go` — add the private rune bound and apply it to both repeat hold requirements only.
|
||||
- [ ] `apps/edge/internal/openai/stream_gate_filters_test.go` — add `TestOpenAIRepeatHoldBufferContract` for participant, composed-plan, evidence-window, and schema non-regression assertions.
|
||||
- [ ] `agent-task/provider_stream_buffer_compat/CODE_REVIEW-cloud-G05.md` — record actual implementation decisions and verification output.
|
||||
|
||||
#### Test Strategy
|
||||
|
||||
Write a regression because this is a bug fix. The test must use production registration/resolution rather than only direct constructors, so omission of the automatically registered action sibling fails.
|
||||
|
||||
#### Verification
|
||||
|
||||
```bash
|
||||
gofmt -d apps/edge/internal/openai/stream_gate_filters.go apps/edge/internal/openai/stream_gate_filters_test.go
|
||||
go test -count=1 ./apps/edge/internal/openai -run '^Test(OpenAIRepeatHoldBufferContract|OpenAIOutputFilterRegistrations)$'
|
||||
```
|
||||
|
||||
Expected: no formatting diff; fresh tests prove the repeat pair composes to 1,048,576 runes while the evidence window and schema bound remain unchanged.
|
||||
|
||||
### [STREAM_BUFFER-2] Large semantic-event exact-wire tunnel regression
|
||||
|
||||
#### Problem
|
||||
|
||||
`apps/edge/internal/openai/stream_gate_tunnel_codec.go:239-286` converts a complete SSE frame into one or more semantic events and attaches the original wire frame to the first releasable event. `packages/go/streamgate/evidence_tail.go:1564-1582` rejects the whole event before evaluation when its rune count exceeds the composed hold bound. Existing production lifecycle coverage at `apps/edge/internal/openai/stream_gate_pipeline_test.go:612-720` uses small payloads and does not reproduce the observed greater-than-4,096 event.
|
||||
|
||||
Before:
|
||||
|
||||
```go
|
||||
// apps/edge/internal/openai/stream_gate_pipeline_test.go:612-634
|
||||
func TestStreamGateConfiguredRepeatActionSplitLifecycle(t *testing.T) {
|
||||
// Production registry/Core/tunnel coverage exists, but its frames are small.
|
||||
gateCfg := config.StreamEvidenceGateConf{
|
||||
Enabled: true,
|
||||
Filters: []config.StreamGateFilterPolicyConf{{
|
||||
Filter: config.StreamGateFilterRepeatGuard,
|
||||
Enforcement: config.StreamGateFilterEnforcementBlocking,
|
||||
HoldEvidenceRunes: 500,
|
||||
}},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Solution
|
||||
|
||||
- Add `TestStreamGateConfiguredRepeatGuardLargeTunnelEvent` beside the production tunnel lifecycle tests.
|
||||
- Use four cases: Chat content (`choices[].delta.content`), Chat reasoning (`choices[].delta.reasoning_content`), Responses output text (`response.output_text.delta`), and Responses reasoning (`response.reasoning_text.delta`).
|
||||
- Encode one SSE JSON frame with 5,000 distinct Korean runes per case, followed by endpoint-native terminal wire. The payload is above the old default, below the new bound, and cannot trigger repeat matching.
|
||||
- Configure only blocking `repeat_guard` with the normal 500-rune evidence window and no schema metadata. Drive the production registry, tunnel event source, Core runtime, and tunnel release sink with buffered `ProviderTunnelFrame` fixtures.
|
||||
- Require `runtime.Run` success, one HTTP 200 start, one successful terminal, zero recovery dispatches, and caller body bytes exactly equal to the concatenated provider frames.
|
||||
- Keep each provider frame whole; do not add codec fragmentation or a replacement serializer.
|
||||
|
||||
After:
|
||||
|
||||
```go
|
||||
func TestStreamGateConfiguredRepeatGuardLargeTunnelEvent(t *testing.T) {
|
||||
payload := uniqueKoreanRunes(5000)
|
||||
tests := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
frame []byte
|
||||
}{
|
||||
{name: "chat content", endpoint: openAIRebuildEndpointChat, frame: chatContentFrame(payload)},
|
||||
{name: "chat reasoning", endpoint: openAIRebuildEndpointChat, frame: chatReasoningFrame(payload)},
|
||||
{name: "responses output text", endpoint: openAIRebuildEndpointResponses, frame: responsesTextFrame(payload)},
|
||||
{name: "responses reasoning", endpoint: openAIRebuildEndpointResponses, frame: responsesReasoningFrame(payload)},
|
||||
}
|
||||
// Each case runs production registry -> tunnel source -> Core -> tunnel sink
|
||||
// and compares caller bytes with the original semantic and terminal frames.
|
||||
}
|
||||
```
|
||||
|
||||
Helper names are illustrative. Build the JSON/SSE fixtures locally with `encoding/json` or existing imports; add no dependency.
|
||||
|
||||
#### Modified Files and Checklist
|
||||
|
||||
- [ ] `apps/edge/internal/openai/stream_gate_pipeline_test.go` — add the four-case production runtime regression and exact-wire assertions.
|
||||
- [ ] `agent-task/provider_stream_buffer_compat/CODE_REVIEW-cloud-G05.md` — record actual regression output and any justified deviation.
|
||||
|
||||
#### Test Strategy
|
||||
|
||||
Write the integration regression in the existing pipeline test file. The distinct 5,000-rune payload exercises UTF-8 rune accounting and both endpoint and semantic-kind axes without changing filter semantics.
|
||||
|
||||
#### Verification
|
||||
|
||||
```bash
|
||||
gofmt -d apps/edge/internal/openai/stream_gate_pipeline_test.go
|
||||
go test -count=1 ./apps/edge/internal/openai -run '^TestStreamGateConfiguredRepeatGuardLargeTunnelEvent$'
|
||||
```
|
||||
|
||||
Expected: no formatting diff; all four fresh subtests return the original wire once with HTTP 200, one successful terminal, and no recovery dispatch.
|
||||
|
||||
## Modified Files Summary
|
||||
|
||||
| File | Items |
|
||||
|------|-------|
|
||||
| `apps/edge/internal/openai/stream_gate_filters.go` | STREAM_BUFFER-1 |
|
||||
| `apps/edge/internal/openai/stream_gate_filters_test.go` | STREAM_BUFFER-1 |
|
||||
| `apps/edge/internal/openai/stream_gate_pipeline_test.go` | STREAM_BUFFER-2 |
|
||||
| `agent-task/provider_stream_buffer_compat/CODE_REVIEW-cloud-G05.md` | STREAM_BUFFER-1, STREAM_BUFFER-2 implementation evidence |
|
||||
|
||||
## Dependencies and Execution Order
|
||||
|
||||
1. Implement `STREAM_BUFFER-1` so the production registry resolves the corrected composed bound.
|
||||
2. Implement `STREAM_BUFFER-2` against that registry and retain exact provider wire.
|
||||
3. Run focused and final verification, then fill the active review evidence.
|
||||
|
||||
## Final Verification
|
||||
|
||||
```bash
|
||||
command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT && go env GOMOD
|
||||
gofmt -d apps/edge/internal/openai/stream_gate_filters.go apps/edge/internal/openai/stream_gate_filters_test.go apps/edge/internal/openai/stream_gate_pipeline_test.go
|
||||
go test -count=1 ./apps/edge/internal/openai -run '^Test(OpenAIRepeatHoldBufferContract|StreamGateConfiguredRepeatGuardLargeTunnelEvent)$'
|
||||
go test -race -count=1 ./packages/go/streamgate ./apps/edge/internal/openai ./packages/go/config
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: host Go identity is unchanged; formatting is clean; fresh focused and race suites pass; all four large-event tunnel variants preserve the exact provider wire with one successful terminal; and the diff has no whitespace errors. Proto generation and general mock/provider smoke are excluded because no proto/config/route is changed and those commands do not activate this repeat-policy failure condition.
|
||||
|
||||
After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Milestone Work Log
|
||||
|
||||
> Dispatcher-owned execution timeline. Workers and reviewers do not edit this file.
|
||||
|
||||
| seq | time | event | task | role | attempt | model | result | locator |
|
||||
|---:|---|---|---|---|---:|---|---|---|
|
||||
| 1 | 26-07-30 06:34:32 | START | provider_stream_buffer_compat | worker | 0 | agy/Gemini 3.6 Flash (High) | running | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260729T213432Z__provider_stream_buffer_compat__p1__worker__a00/locator.json |
|
||||
| 2 | 26-07-30 06:37:03 | FINISH | provider_stream_buffer_compat | worker | 0 | agy/Gemini 3.6 Flash (High) | succeeded:0 | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260729T213432Z__provider_stream_buffer_compat__p1__worker__a00/locator.json |
|
||||
| 3 | 26-07-30 06:37:03 | START | provider_stream_buffer_compat | review | 0 | codex/gpt-5.6-sol xhigh | running | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260729T213703Z__provider_stream_buffer_compat__p1__review__a00/locator.json |
|
||||
| 4 | 26-07-30 06:43:07 | FINISH | provider_stream_buffer_compat | review | 0 | codex/gpt-5.6-sol xhigh | succeeded:0 | /config/workspace/iop/.git/agent-task-dispatcher/runs/20260729T213703Z__provider_stream_buffer_compat__p1__review__a00/locator.json |
|
||||
|
|
@ -122,6 +122,10 @@ func newOpenAIOutputFilter(kind openAIOutputFilterKind, holdRunes, priority int,
|
|||
return filter, nil
|
||||
}
|
||||
|
||||
// A provider may coalesce one valid content/reasoning delta above Core's
|
||||
// default hold size. This bound is measured in runes and does not widen evidence.
|
||||
const openAIRepeatHoldMaxBufferRunes = 1 << 20
|
||||
|
||||
// Applies is execution-path-neutral because endpoint codecs expose the same
|
||||
// semantic event kinds for normalized and provider-tunnel attempts.
|
||||
func (f *openAIOutputFilter) Applies(streamgate.FilterContext) bool {
|
||||
|
|
@ -132,32 +136,40 @@ func (f *openAIOutputFilter) Applies(streamgate.FilterContext) bool {
|
|||
func (f *openAIOutputFilter) HoldRequirement(streamgate.FilterContext) streamgate.FilterHoldRequirement {
|
||||
switch f.kind {
|
||||
case openAIOutputFilterRepeatGuard:
|
||||
req, err := streamgate.NewFilterHoldRequirementRolling(
|
||||
req, err := streamgate.NewFilterHoldRequirementRollingWithMaxBuffer(
|
||||
f.channel,
|
||||
[]streamgate.EventKind{
|
||||
streamgate.EventKindTextDelta,
|
||||
streamgate.EventKindReasoningDelta,
|
||||
},
|
||||
f.holdRunes,
|
||||
openAIRepeatHoldMaxBufferRunes,
|
||||
)
|
||||
if err != nil {
|
||||
req, _ = streamgate.NewFilterHoldRequirementRolling(f.channel, []streamgate.EventKind{streamgate.EventKindTextDelta}, f.holdRunes)
|
||||
req, _ = streamgate.NewFilterHoldRequirementRollingWithMaxBuffer(
|
||||
f.channel,
|
||||
[]streamgate.EventKind{streamgate.EventKindTextDelta},
|
||||
f.holdRunes,
|
||||
openAIRepeatHoldMaxBufferRunes,
|
||||
)
|
||||
}
|
||||
return req
|
||||
case openAIOutputFilterRepeatActionGuard:
|
||||
req, err := streamgate.NewFilterHoldRequirementTerminalGate(
|
||||
req, err := streamgate.NewFilterHoldRequirementTerminalGateWithMaxBuffer(
|
||||
f.channel,
|
||||
[]streamgate.EventKind{
|
||||
streamgate.EventKindToolCallFragment,
|
||||
streamgate.EventKindTerminal,
|
||||
},
|
||||
streamgate.EventKindTerminal,
|
||||
openAIRepeatHoldMaxBufferRunes,
|
||||
)
|
||||
if err != nil {
|
||||
req, _ = streamgate.NewFilterHoldRequirementTerminalGate(
|
||||
req, _ = streamgate.NewFilterHoldRequirementTerminalGateWithMaxBuffer(
|
||||
f.channel,
|
||||
[]streamgate.EventKind{streamgate.EventKindToolCallFragment},
|
||||
streamgate.EventKindTerminal,
|
||||
openAIRepeatHoldMaxBufferRunes,
|
||||
)
|
||||
}
|
||||
return req
|
||||
|
|
|
|||
|
|
@ -1037,3 +1037,106 @@ func TestOpenAIRepeatAndSchemaFiltersPassCleanEpoch(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIRepeatHoldBufferContract(t *testing.T) {
|
||||
gateCfg := outputFilterGateCfg(config.StreamGateFilterEnforcementBlocking)
|
||||
fctxNoScheme := openAIOutputFilterContext{endpoint: openAIRebuildEndpointChat, requestRef: "openai.snap.1"}
|
||||
regsNoScheme, policiesNoScheme, err := openAIOutputFilterRegistrations(gateCfg, fctxNoScheme)
|
||||
if err != nil {
|
||||
t.Fatalf("openAIOutputFilterRegistrations(no scheme): %v", err)
|
||||
}
|
||||
|
||||
snap, err := streamgate.NewFilterRegistrySnapshot(streamGateConfigGeneration, regsNoScheme, policiesNoScheme)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFilterRegistrySnapshot: %v", err)
|
||||
}
|
||||
reqCtx, err := streamgate.NewRequestFilterContext(
|
||||
streamGateConfigGeneration, "attempt.1", streamGateEnvironment,
|
||||
openAIRebuildEndpointChat, openAIRebuildFamily, "",
|
||||
streamgate.CommitStateTransportUncommitted, false, false, "",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRequestFilterContext: %v", err)
|
||||
}
|
||||
reqSnap, err := snap.BeginRequest(reqCtx)
|
||||
if err != nil {
|
||||
t.Fatalf("BeginRequest: %v", err)
|
||||
}
|
||||
target, err := streamgate.NewAttemptTarget("client-model", "ornith:35b", "prov-a", "normalized",
|
||||
[]string{"output.repeat_guard", "output.provider_error"})
|
||||
if err != nil {
|
||||
t.Fatalf("NewAttemptTarget: %v", err)
|
||||
}
|
||||
resolved, err := reqSnap.ResolveAttempt(target)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveAttempt: %v", err)
|
||||
}
|
||||
|
||||
plan, err := streamgate.NewEvidencePlanFromResolvedFilters(resolved)
|
||||
if err != nil {
|
||||
t.Fatalf("NewEvidencePlanFromResolvedFilters: %v", err)
|
||||
}
|
||||
|
||||
const wantBound = openAIRepeatHoldMaxBufferRunes // 1048576
|
||||
if got := plan.MaxBufferRunes(streamGateChannelDefault); got != wantBound {
|
||||
t.Errorf("composed channel maxBufferRunes = %d, want %d", got, wantBound)
|
||||
}
|
||||
|
||||
bindings := plan.BindingsForChannel(streamGateChannelDefault)
|
||||
foundRepeat := false
|
||||
foundAction := false
|
||||
for _, b := range bindings {
|
||||
req := b.Requirement()
|
||||
switch b.FilterID() {
|
||||
case openAIRepeatGuardFilterID:
|
||||
foundRepeat = true
|
||||
if got := req.MaxBufferRunes(); got != wantBound {
|
||||
t.Errorf("repeat_guard MaxBufferRunes = %d, want %d", got, wantBound)
|
||||
}
|
||||
if got := req.EvidenceRunes(); got != 500 {
|
||||
t.Errorf("repeat_guard EvidenceRunes = %d, want 500", got)
|
||||
}
|
||||
case openAIRepeatActionGuardFilterID:
|
||||
foundAction = true
|
||||
if got := req.MaxBufferRunes(); got != wantBound {
|
||||
t.Errorf("repeat_action_guard MaxBufferRunes = %d, want %d", got, wantBound)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundRepeat {
|
||||
t.Error("missing repeat_guard binding in compiled plan")
|
||||
}
|
||||
if !foundAction {
|
||||
t.Error("missing repeat_action_guard binding in compiled plan")
|
||||
}
|
||||
|
||||
// Verify standalone schema requirement retains default bound (4096).
|
||||
regsScheme, policiesScheme, err := openAIOutputFilterRegistrations(gateCfg, schemaOutputFilterContext("openai.snap.1"))
|
||||
if err != nil {
|
||||
t.Fatalf("openAIOutputFilterRegistrations(scheme): %v", err)
|
||||
}
|
||||
snapScheme, err := streamgate.NewFilterRegistrySnapshot(streamGateConfigGeneration, regsScheme, policiesScheme)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFilterRegistrySnapshot(scheme): %v", err)
|
||||
}
|
||||
reqSnapScheme, err := snapScheme.BeginRequest(reqCtx)
|
||||
if err != nil {
|
||||
t.Fatalf("BeginRequest(scheme): %v", err)
|
||||
}
|
||||
targetScheme, err := streamgate.NewAttemptTarget("client-model", "ornith:35b", "prov-a", "normalized",
|
||||
[]string{"output.repeat_guard", "output.schema_gate", "output.provider_error"})
|
||||
if err != nil {
|
||||
t.Fatalf("NewAttemptTarget(scheme): %v", err)
|
||||
}
|
||||
resolvedScheme, err := reqSnapScheme.ResolveAttempt(targetScheme)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveAttempt(scheme): %v", err)
|
||||
}
|
||||
for _, rf := range resolvedScheme {
|
||||
if rf.FilterID() == openAISchemaGateFilterID {
|
||||
if got := rf.HoldRequirement().MaxBufferRunes(); got != streamgate.DefaultMaxBufferRunes {
|
||||
t.Errorf("schema_gate MaxBufferRunes = %d, want default %d", got, streamgate.DefaultMaxBufferRunes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
edgeservice "iop/apps/edge/internal/service"
|
||||
"iop/packages/go/config"
|
||||
|
|
@ -1038,3 +1039,157 @@ func releaseTunnelCodecEvents(t *testing.T, state *openAITunnelCodecState, event
|
|||
}
|
||||
return w.body.String()
|
||||
}
|
||||
|
||||
func TestStreamGateConfiguredRepeatGuardLargeTunnelEvent(t *testing.T) {
|
||||
payload := uniqueKoreanRunes(5000)
|
||||
if got := utf8.RuneCountInString(payload); got != 5000 {
|
||||
t.Fatalf("uniqueKoreanRunes count = %d, want 5000", got)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
semanticFrame []byte
|
||||
terminalFrame []byte
|
||||
}{
|
||||
{
|
||||
name: "chat content",
|
||||
endpoint: openAIRebuildEndpointChat,
|
||||
semanticFrame: []byte(fmt.Sprintf("data: {\"id\":\"chatcmpl-1\",\"object\":\"chat.completion.chunk\",\"created\":1700000000,\"model\":\"served-model\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"%s\"}}]}\n\n", payload)),
|
||||
terminalFrame: []byte("data: [DONE]\n\n"),
|
||||
},
|
||||
{
|
||||
name: "chat reasoning",
|
||||
endpoint: openAIRebuildEndpointChat,
|
||||
semanticFrame: []byte(fmt.Sprintf("data: {\"id\":\"chatcmpl-1\",\"object\":\"chat.completion.chunk\",\"created\":1700000000,\"model\":\"served-model\",\"choices\":[{\"index\":0,\"delta\":{\"reasoning_content\":\"%s\"}}]}\n\n", payload)),
|
||||
terminalFrame: []byte("data: [DONE]\n\n"),
|
||||
},
|
||||
{
|
||||
name: "responses output text",
|
||||
endpoint: openAIRebuildEndpointResponses,
|
||||
semanticFrame: []byte(fmt.Sprintf("data: {\"type\":\"response.output_text.delta\",\"delta\":\"%s\"}\n\n", payload)),
|
||||
terminalFrame: []byte("data: [DONE]\n\n"),
|
||||
},
|
||||
{
|
||||
name: "responses reasoning",
|
||||
endpoint: openAIRebuildEndpointResponses,
|
||||
semanticFrame: []byte(fmt.Sprintf("data: {\"type\":\"response.reasoning_text.delta\",\"delta\":\"%s\"}\n\n", payload)),
|
||||
terminalFrame: []byte("data: [DONE]\n\n"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fault := 3
|
||||
gateCfg := config.StreamEvidenceGateConf{
|
||||
Enabled: true,
|
||||
MaxRequestFaultRecovery: &fault,
|
||||
Filters: []config.StreamGateFilterPolicyConf{{
|
||||
Filter: config.StreamGateFilterRepeatGuard,
|
||||
Enforcement: config.StreamGateFilterEnforcementBlocking,
|
||||
Priority: 10,
|
||||
HoldEvidenceRunes: 500,
|
||||
}},
|
||||
}
|
||||
service := &providerFakeRunService{}
|
||||
srv := NewServer(config.EdgeOpenAIConf{
|
||||
Adapter: "openai-compat", Target: "served-model", TimeoutSec: 15,
|
||||
StreamEvidenceGate: gateCfg,
|
||||
}, service, nil)
|
||||
observations := &recordingOpenAIObservationSink{}
|
||||
srv.SetObservationSink(observations)
|
||||
|
||||
var rawReq []byte
|
||||
if tt.endpoint == openAIRebuildEndpointChat {
|
||||
rawReq = []byte(`{"model":"client-model","messages":[{"role":"user","content":"hi"}]}`)
|
||||
} else {
|
||||
rawReq = []byte(`{"model":"client-model","input":"hi"}`)
|
||||
}
|
||||
route := routeDispatch{Adapter: "openai-compat", Target: "served-model", TimeoutSec: 15}
|
||||
requestCtx := newTestRequestContext(t, route, rawReq)
|
||||
req := openAITunnelStreamGateRequest{
|
||||
route: route, ingress: requestCtx.ingress, endpoint: tt.endpoint,
|
||||
method: http.MethodPost, path: "/v1/" + tt.endpoint, stream: true,
|
||||
modelGroupKey: "client-model",
|
||||
authorize: func(context.Context) (map[string]string, error) { return nil, nil },
|
||||
rewriteBody: func(body []byte, _ string) ([]byte, error) { return body, nil },
|
||||
}
|
||||
frames := make(chan *iop.ProviderTunnelFrame)
|
||||
handle := &fakeTunnelHandle{
|
||||
dispatch: edgeservice.RunDispatch{
|
||||
RunID: "large-" + tt.name, ModelGroupKey: "client-model",
|
||||
Adapter: "openai-compat", Target: "served-model",
|
||||
ProviderID: "provider-a", ExecutionPath: string(edgeservice.ProviderPoolPathTunnel),
|
||||
},
|
||||
frames: frames,
|
||||
}
|
||||
fctx, err := srv.openAITunnelOutputFilterContext(req)
|
||||
if err != nil {
|
||||
t.Fatalf("openAITunnelOutputFilterContext: %v", err)
|
||||
}
|
||||
registry, err := openAIStreamGateRegistrySnapshotFor(gateCfg, fctx)
|
||||
if err != nil {
|
||||
t.Fatalf("openAIStreamGateRegistrySnapshotFor: %v", err)
|
||||
}
|
||||
writer := newSynchronizedTunnelLifecycleWriter()
|
||||
sink := newOpenAITunnelReleaseSink(writer, writer)
|
||||
runtime, _, err := srv.buildOpenAITunnelStreamGateRuntime(req, handle, sink, registry)
|
||||
if err != nil {
|
||||
t.Fatalf("buildOpenAITunnelStreamGateRuntime: %v", err)
|
||||
}
|
||||
runDone := make(chan error, 1)
|
||||
go func() {
|
||||
runDone <- runtime.Run(t.Context())
|
||||
}()
|
||||
|
||||
frames <- &iop.ProviderTunnelFrame{
|
||||
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_RESPONSE_START,
|
||||
StatusCode: http.StatusOK,
|
||||
Headers: map[string]string{"Content-Type": "text/event-stream"},
|
||||
}
|
||||
frames <- &iop.ProviderTunnelFrame{
|
||||
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
|
||||
Body: tt.semanticFrame,
|
||||
}
|
||||
frames <- &iop.ProviderTunnelFrame{
|
||||
Kind: iop.ProviderTunnelFrameKind_PROVIDER_TUNNEL_FRAME_KIND_BODY,
|
||||
Body: tt.terminalFrame,
|
||||
}
|
||||
close(frames)
|
||||
|
||||
select {
|
||||
case runErr := <-runDone:
|
||||
if runErr != nil {
|
||||
t.Fatalf("runtime.Run: %v", runErr)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("runtime did not finish after large event evaluation")
|
||||
}
|
||||
if closeErr := runtime.CloseRequestResources(t.Context(), true); closeErr != nil {
|
||||
t.Fatalf("CloseRequestResources: %v", closeErr)
|
||||
}
|
||||
|
||||
status, headerCalls, body := writer.snapshot()
|
||||
if headerCalls != 1 {
|
||||
t.Fatalf("response-start commits = %d, want 1", headerCalls)
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("HTTP status = %d, want %d", status, http.StatusOK)
|
||||
}
|
||||
terminalCommitted, terminalSuccess := sink.terminalStatus()
|
||||
if !terminalCommitted || !terminalSuccess {
|
||||
t.Fatalf("terminal status = (committed=%v, success=%v), want (true, true)", terminalCommitted, terminalSuccess)
|
||||
}
|
||||
wantBody := string(tt.semanticFrame) + string(tt.terminalFrame)
|
||||
if string(body) != wantBody {
|
||||
t.Fatalf("released body length = %d, want exact wire length %d", len(body), len(wantBody))
|
||||
}
|
||||
|
||||
for _, obs := range observations.Snapshot() {
|
||||
if obs.Kind() == streamgate.ObservationKindRecoveryDispatched {
|
||||
t.Fatalf("unexpected recovery dispatched observation: %+v", obs)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue