<!-- task=m-iop-owned-single-request-agent-execution/23+22_error_cancel plan=4 tag=REVIEW_API milestone-task=error-cancel -->

# Make child deadline classification request-authoritative

## For the Implementing Agent

Fill every implementation-owned section in `CODE_REVIEW-cloud-G10.md` after coding. Run the verification commands exactly, paste actual output, keep both active files in place, and report ready for review; finalization belongs only to the code-review skill. If blocked, record only the exact blocker, attempted commands/output, and resume condition 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 preceding correction made the request monitor and executor-return path classify the immutable request wall-clock as `error/budget`. Internal-tool and artifact operations still inspect their derived child context first, so the same parent deadline can nondeterministically become `error/timeout`. The service must classify the authoritative parent request state before any child/stage deadline while retaining timeout for a genuinely earlier stage or operation deadline.

## Archive Evidence Snapshot

- `agent-task/m-iop-owned-single-request-agent-execution/23+22_error_cancel/code_review_cloud_G10_3.log` ended in `FAIL` with Required R1: child `DeadlineExceeded` paths in `single_request_tool_loop.go` and `single_request_artifact.go` can override request wall-clock ownership.
- Fresh review verification passed the submitted focused, compatibility, full Edge, and SDD race suites, but a separate race-enabled reproduction over 2,000 real internal-tool wall-clock expiries produced 67–99 `error/timeout` terminals per 200-request run. The passing submitted suite therefore does not cover the failing child path.
- The provider-cancellation, upstream HTTP-status, and executor/monitor request-budget corrections from `plan_cloud_G10_3.log` remain satisfied and are regression-only scope here.
- `agent-task/archive/2026/08/m-iop-owned-single-request-agent-execution/22+21_executor_activation/complete.log` satisfies predecessor subtask 22. Milestone S12 external Claude smoke remains the separate `claude-smoke` task and is not a substitute for S11 deterministic coverage.

## Finding Resolution Map

| Finding | Mode | Fix and changed precondition |
|---------|------|------------------------------|
| Required R1 | direct-fix | Centralize parent-first child failure classification in `apps/edge/internal/service/single_request.go`; consume it from `apps/edge/internal/service/single_request_tool_loop.go` and `apps/edge/internal/service/single_request_artifact.go`; add real wall-clock race regressions in `apps/edge/internal/service/single_request_tool_loop_test.go` and `apps/edge/internal/service/single_request_artifact_test.go`. The precondition changes from “child context expiry can win” to “caller cancellation, then immutable request budget, then genuinely earlier child timeout is deterministic.” |

`ownership_closed=true`: Required R1 is repository-fixable in this packet and has no unordered external dependency.

## Analysis

### Files Read

Production files read in full:

- `apps/edge/internal/service/single_request.go`
- `apps/edge/internal/service/single_request_tool_loop.go`
- `apps/edge/internal/service/single_request_artifact.go`

Test files read in full:

- `apps/edge/internal/service/single_request_test.go`
- `apps/edge/internal/service/single_request_tool_loop_test.go`
- `apps/edge/internal/service/single_request_artifact_test.go`
- `apps/edge/internal/service/single_request_observation_test.go`

### SDD Criteria

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/iop-owned-single-request-agent-execution/SDD.md`; status approved and unlocked.
- Milestone metadata: `milestone-task=error-cancel`.
- Target: Acceptance Scenario S11 and its Evidence Map row for the bounded error/cancel/length terminal matrix.
- S11 requires provider/tool timeout, request budget, and caller disconnect to converge to the correct single terminal without fallback, a second external request, or partial success. That requirement drives the parent-first classifier, real tool/artifact race regressions, exact-one-terminal assertions, and the full SDD race suite below.

### Verification Context

- No separate verification handoff was supplied. Repository-native fallback evidence came from `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, the source/tests listed above, the active milestone/SDD, the matching living specs and Anthropic outer contract, and fresh review commands recorded in `code_review_cloud_G10_3.log`.
- Fresh review evidence: focused and compatibility race tests, `go vet`, all Edge tests, the approved SDD common race suite, protobuf regeneration/hash comparison, deterministic contract/symbol searches, and `git diff --check` passed.
- Contradicting evidence: a temporary test using the real internal-tool path with equal request/stage limits repeatedly observed mixed budget and timeout terminals under `-race`; the temporary file was removed after reproduction.
- Preconditions: local repository root `/config/workspace/iop-s0`; Go reports `go1.26.2 linux/arm64`; fresh runs use `-count=1` or `-count=10` to bypass the Go test cache.
- Constraints: keep the closed public terminal vocabulary, raw-free observations, exactly-one terminal, no new dependency, and no protocol/schema change. Full Claude/Mac execution is S12 and excluded from this S11 follow-up.
- Confidence: high for the root cause because the mixed terminal class was reproduced through the production tool path and matches both child-first branches by inspection. Artifact needs its own real regression because its analogous branch was not covered by the temporary reproduction.

### Test Coverage Gaps

- Existing `TestSingleRequestRequestWallClockBudgetDisposition` covers an executor waiting directly on the parent request context, not a derived internal-tool context.
- Existing `TestSingleRequestInternalToolLoopStageDeadline` and deadline observation tests cover a genuinely earlier stage timeout, which must remain timeout.
- No existing test forces request wall-clock expiry while a real internal workspace tool is blocked and asserts budget consistently across repeated race runs.
- No existing test forces request wall-clock expiry while a real artifact operation is blocked and asserts budget consistently.
- The new regressions must assert service error sentinel, public terminal class, observation class, and exactly one terminal while retaining the earlier-stage timeout control.

### Symbol References

No symbol is renamed or removed. The new private classifier is consumed only by the internal-tool and artifact failure paths; existing public interfaces and terminal enums remain unchanged.

### Split Judgment

Keep one packet: tool and artifact are two variants of the same service-owned parent-vs-child deadline invariant and must share one ordering rule. Splitting them would permit divergent classifiers and would not independently prove S11. Subtask predecessor 22 is satisfied by `agent-task/archive/2026/08/m-iop-owned-single-request-agent-execution/22+21_executor_activation/complete.log`.

### Scope Rationale

- Do not change `apps/edge/internal/openai`: the prior cancellation, HTTP status, and executor-envelope fixes passed fresh regressions; this packet only reruns them.
- Do not change Node wire, protobuf, config, specs, or contracts: the defect is local service classification and the closed contract already requires the intended result.
- Do not add retry, fallback, another external request, or new terminal values.
- Do not perform S12 `claude-smoke`; it remains a separate milestone task after S11 is correct.

### Final Routing

- `evaluation_mode=isolated-reassessment`; `finalizer=finalize-task-policy.sh`; `finalizer_mode=pair`.
- Build: closure complete; scores `scope_coupling=2`, `state_concurrency=2`, `blast_irreversibility=2`, `evidence_diagnosis=2`, `verification_complexity=2`; grade `G10`; base/final basis `grade-boundary`; route `cloud`; filename `PLAN-cloud-G10.md`.
- Review: closure complete; `official-review`; the same five scores total `G10`; route `cloud`; filename `CODE_REVIEW-cloud-G10.md`.
- `large_indivisible_context=false`; positive loop risks: `temporal_state`, `concurrent_consistency`, `boundary_contract`, `variant_product`; `loop_risk_count=4`; `risk_boundary_matched=true`.
- Recovery: `review_rework_count=2`; `evidence_integrity_failure=true`; `recovery_boundary_matched=true`.
- Capability gap: none. Canonical worker/reviewer catalogs are `worker/cloud/G10` and `review/cloud/G10`.

## Implementation Checklist

- [ ] Add one service-owned parent-first child-operation classifier and route internal-tool and artifact failure/observation paths through it without changing genuine earlier-stage timeout behavior.
- [ ] Add repeated race-enabled real internal-tool and artifact request-wall-clock regressions that assert budget, raw-free observation, and exactly one terminal.
- [ ] Run focused race, compatibility, full Edge/SDD, protobuf reproducibility, deterministic contract/symbol, and diff-hygiene verification freshly.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [REVIEW_API-1] Make child operation deadlines request-authoritative

**Problem**

`apps/edge/internal/service/single_request_tool_loop.go:233-260` reduces the derived tool context to timeout before consulting the service parent:

```go
233 func singleRequestToolOutcome(ctx context.Context) (singleRequestOutcome, singleRequestErrorClass) {
234     if deadline, ok := ctx.Deadline(); ok && !time.Now().Before(deadline) {
235         return singleRequestOutcomeError, singleRequestErrorClassTimeout
...
256 func (h *singleRequestHandle) failInternalWorkspaceToolOutcome(ctx context.Context, err error) {
257     deadline, hasDeadline := ctx.Deadline()
258     if errors.Is(ctx.Err(), context.DeadlineExceeded) || hasDeadline && !time.Now().Before(deadline) {
259         h.failInternalWorkspaceToolWithErrorClass(ErrSingleRequestInternalToolBudget, singleRequestErrorClassTimeout)
```

`apps/edge/internal/service/single_request_artifact.go:196-208` repeats the same child-first ordering:

```go
196 func (h *singleRequestHandle) failSingleRequestArtifact(ctx context.Context, err error) {
...
202     if ctx != nil && errors.Is(ctx.Err(), context.Canceled) {
203         h.cancelLocked()
...
206     if errors.Is(err, context.DeadlineExceeded) || ctx != nil && errors.Is(ctx.Err(), context.DeadlineExceeded) || errors.Is(err, ErrSingleRequestInternalArtifactBudget) {
207         h.failLockedWithErrorClass(ErrSingleRequestInternalArtifactBudget, singleRequestErrorClassTimeout)
```

Because both operation contexts inherit `h.execCtx`, request wall-clock expiry satisfies those checks and races the request monitor.

**Solution**

Add one private service-owned classification helper in `single_request.go` and use it for both terminal and observation decisions. Its order must be caller parent cancellation, immutable request wall-clock exhaustion (`h.execCtx`/`h.requestDeadline`) as budget, then derived operation cancellation/deadline as cancel/timeout, then the typed operation fallback. Do not infer request budget from an arbitrary raw `context.DeadlineExceeded` while the service parent is live.

The intended shape is:

```go
func (h *singleRequestHandle) classifyChildOperationContext(ctx context.Context) (singleRequestOutcome, singleRequestErrorClass) {
    switch {
    case h.callerCtx.Err() != nil:
        return singleRequestOutcomeCancel, singleRequestErrorClassCancel
    case errors.Is(h.execCtx.Err(), context.DeadlineExceeded) || requestDeadlineReached(h.requestDeadline):
        return singleRequestOutcomeError, singleRequestErrorClassInternalToolBudget
    case ctx != nil && errors.Is(ctx.Err(), context.Canceled):
        return singleRequestOutcomeCancel, singleRequestErrorClassCancel
    case childDeadlineReached(ctx):
        return singleRequestOutcomeError, singleRequestErrorClassTimeout
    default:
        return singleRequestOutcomeError, singleRequestErrorClassInternalToolFailed
    }
}
```

Translate `singleRequestErrorClassInternalToolBudget` to the existing budget terminal sentinel, `singleRequestErrorClassTimeout` to the existing timeout terminal, and cancellation through the existing cancel owner. The helper names may differ, but there must be one ordering source of truth used by tool and artifact paths. Preserve the independent stage timer behavior when `StageTimeoutMS < WallClockMS`.

**Modified Files and Checklist**

- [ ] Add the shared parent-first classifier in `apps/edge/internal/service/single_request.go`.
- [ ] Replace child-only outcome/failure classification in `apps/edge/internal/service/single_request_tool_loop.go`.
- [ ] Replace child-only artifact failure classification in `apps/edge/internal/service/single_request_artifact.go`.
- [ ] Add `TestSingleRequestInternalToolRequestWallClockBudgetOwnership` in `apps/edge/internal/service/single_request_tool_loop_test.go`.
- [ ] Add `TestSingleRequestArtifactRequestWallClockBudgetOwnership` in `apps/edge/internal/service/single_request_artifact_test.go`.

**Test Strategy**

Write both regressions. The internal-tool test must enter the real workspace tool path, block until its inherited context expires, repeat enough requests to expose monitor/tool races, and assert `ErrSingleRequestInternalToolBudget`, public `error/budget`, observation `internal_tool_budget`, and one terminal. The artifact test must do the same through a real `WriteInternalArtifact` or `ReadInternalArtifact` operation. Keep `TestSingleRequestInternalToolLoopStageDeadline` and `TestSingleRequestObservationDeadlineClassifications/stage timer expiry is observed as timeout` as controls proving earlier child/stage deadlines remain timeout.

**Verification**

```sh
go test -race ./apps/edge/internal/service -run '^TestSingleRequest(InternalTool|Artifact)RequestWallClockBudgetOwnership$' -count=10
```

Expected: every repeated real child-path expiry is budget with exactly one terminal; no iteration reports timeout.

## Modified Files Summary

| File | Item |
|------|------|
| `apps/edge/internal/service/single_request.go` | REVIEW_API-1 |
| `apps/edge/internal/service/single_request_tool_loop.go` | REVIEW_API-1 |
| `apps/edge/internal/service/single_request_artifact.go` | REVIEW_API-1 |
| `apps/edge/internal/service/single_request_tool_loop_test.go` | REVIEW_API-1 |
| `apps/edge/internal/service/single_request_artifact_test.go` | REVIEW_API-1 |
| `agent-task/m-iop-owned-single-request-agent-execution/23+22_error_cancel/CODE_REVIEW-cloud-G10.md` | REVIEW_API-1 evidence |

## Final Verification

1. Confirm the failed review and predecessor dependency are the exact expected evidence:

```sh
test -f agent-task/m-iop-owned-single-request-agent-execution/23+22_error_cancel/code_review_cloud_G10_3.log && test -f agent-task/archive/2026/08/m-iop-owned-single-request-agent-execution/22+21_executor_activation/complete.log && rg -n 'Overall Verdict: FAIL|Required R1|review_rework_count=2|evidence_integrity_failure=true' agent-task/m-iop-owned-single-request-agent-execution/23+22_error_cancel/code_review_cloud_G10_3.log
```

Expected: both files exist and the archived review prints the FAIL, Required R1, and routing-signal lines.

2. Run the new real child-path ownership regressions repeatedly and freshly:

```sh
go test -race ./apps/edge/internal/service -run '^TestSingleRequest(InternalTool|Artifact)RequestWallClockBudgetOwnership$' -count=10
```

Expected: all runs pass; request wall-clock expiry is always budget, observations are `internal_tool_budget`, and each execution emits one terminal.

3. Prove the parent request path and genuinely earlier stage timeout remain distinct:

```sh
go test -race ./apps/edge/internal/service -run '^TestSingleRequest(RequestWallClockBudgetDisposition|InternalToolLoopStageDeadline|ObservationDeadlineClassifications)$' -count=10
```

Expected: request expiry remains budget and every earlier stage deadline remains timeout across fresh repeated runs.

4. Rerun the complete S11 focused and prior ownership compatibility matrices:

```sh
go test -race ./apps/edge/internal/service ./apps/edge/internal/openai -run 'Test(SingleRequestTerminalDisposition|SingleRequestQualityGate|SingleRequestExecutor(Cancellation|StageFailures|TerminalWaiterCleanup|ParentContextOwnership|RequestBudgetOwnership)|AnthropicSingleRequestErrorCancelMatrix|SingleRequestAnthropicStreamTerminalDisposition|AnthropicSingleRequestLiveContextProviderCancellationBuffered|SingleRequestAnthropicStreamLiveContextProviderCancellation)' -count=1
```

Expected: both packages pass without cached output; prior provider/cancel/status fixes and the corrected service deadline matrix remain compatible.

5. Run Edge static and package regressions:

```sh
go vet ./apps/edge/internal/service ./apps/edge/internal/openai && go test ./apps/edge/... -count=1
```

Expected: vet and every Edge package test pass.

6. Run the approved SDD common race suite:

```sh
go test -race -count=1 ./packages/go/config ./packages/go/streamgate ./apps/edge/internal/openai ./apps/edge/internal/service ./apps/node/internal/node ./apps/node/internal/transport ./apps/node/internal/workspace
```

Expected: every package passes freshly under `-race`.

7. Prove inherited protobuf output remains byte-for-byte reproducible:

```sh
b_before=$(sha256sum proto/gen/iop/runtime.pb.go) && make proto && b_after=$(sha256sum proto/gen/iop/runtime.pb.go) && test "$b_before" = "$b_after" && printf '%s\n' "$b_after"
```

Expected: generation succeeds and prints the unchanged generated-file hash.

8. Confirm the living S11 contract and parent-first classifier references deterministically:

```sh
rg --sort path -n 'caller disconnect|request wall-clock|error-cancel|S11' agent-contract/outer/anthropic-compatible-api.md agent-spec/runtime/edge-node-execution.md agent-spec/input/openai-compatible-surface.md && rg --sort path -n 'classifyChildOperation|requestDeadline|singleRequestErrorClassInternalToolBudget|SingleRequestTerminalErrorBudget|SingleRequestTerminalErrorTimeout' apps/edge/internal/service --glob '*.go'
```

Expected: searches exit 0 and show the living contract/spec rules plus the shared classifier, both consumers, and both regression call sites. If the helper is given another name, record the replacement search and reason in `Deviations from Plan`.

9. Check diff hygiene:

```sh
git diff --check
```

Expected: no output and exit 0.

After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.
