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

# Preserve caller cancellation at late tool admission

## For the Implementing Agent

Fill every implementation-owned section in `CODE_REVIEW-cloud-G08.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

Late tool admission now classifies request-first and stage-first deadline ownership correctly. When the caller is already cancelled, however, the admission branch returns the classifier's cancel class beside a budget sentinel, and `SubmitEnvelope` sends it through the generic failure path before the cancellation monitor acquires the handle lock. Preserve caller cancellation without changing request-budget or genuinely earlier stage-timeout behavior.

## Archive Evidence Snapshot

- The reviewed pair is archived as `agent-task/m-iop-owned-single-request-agent-execution/23+22_error_cancel/plan_cloud_G08_5.log` and `agent-task/m-iop-owned-single-request-agent-execution/23+22_error_cancel/code_review_cloud_G08_5.log`; the review ended in `FAIL` with Required R1 because late tool admission discards the classifier's cancellation outcome and freezes a budget failure.
- A fresh focused reviewer reproducer cancelled `callerCtx`, left the immutable request deadline live, expired the stage deadline, and submitted a valid internal-tool envelope. `SubmitEnvelope` returned `single-request internal tool budget is exhausted` instead of caller cancellation.
- The request-first/stage-first admission regression, real tool/artifact request-budget races, cancellation/terminal compatibility matrix, Edge tests, approved SDD race suite, protobuf reproducibility, deterministic searches, and diff hygiene all passed freshly. They do not cover caller cancellation at late admission.
- `agent-task/archive/2026/08/m-iop-owned-single-request-agent-execution/22+21_executor_activation/complete.log` satisfies predecessor subtask 22. S12 external Claude qualification remains the separate `claude-smoke` task.

## Finding Resolution Map

| Finding | Mode | Fix and changed precondition |
|---------|------|------------------------------|
| Required R1 | direct-fix | Update `apps/edge/internal/service/single_request.go` so a cancel class returned from late internal-tool admission transitions through `cancelLocked` and returns the cancellation sentinel before generic failure handling. Add deterministic late-admission cancellation coverage in `apps/edge/internal/service/single_request_tool_loop_test.go`. The precondition changes from “the tool envelope can freeze budget after caller cancellation” to “the handle lock resolves caller cancellation before any error terminal is frozen.” |

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

## Analysis

### Files Read

Production files:

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

Test files:

- `apps/edge/internal/service/single_request_tool_loop_test.go`

### SDD Criteria

- SDD: `agent-roadmap/sdd/knowledge-tool-optimization-extension/iop-owned-single-request-agent-execution/SDD.md`; status `[승인됨]`, lock released, no user review.
- Milestone metadata: `milestone-task=error-cancel`.
- Target: Acceptance Scenario S11 and its Evidence Map row requiring the budget/error/cancel/length/repetition terminal matrix with bounded, no-partial, no-second-request evidence.
- S11 and the Anthropic contract make caller disconnect a silent `cancelled` terminal owner. This drives the locked late-admission regression, preservation of the two deadline-order cases, the existing in-flight cancel control, and the common race suite.

### Verification Context

- No separate verification handoff was supplied. Repository-native evidence came from `agent-test/local/rules.md`, `agent-test/local/edge-smoke.md`, the active Milestone/SDD, `agent-contract/outer/anthropic-compatible-api.md`, the matching living specs, the source/tests above, and fresh review commands.
- Reviewer preflight: `/config/workspace/iop-s0`, `go1.26.2 linux/arm64`, current dirty worktree preserved, no remote runner or credential required.
- Fresh passes: focused deadline ownership; repeated real tool/artifact request-budget ownership; S11 service/OpenAI compatibility; `go vet`; all Edge tests; the approved common race suite; protobuf hash reproducibility; deterministic contract/symbol searches; `git diff --check`.
- Fresh contradiction: a temporary package-local reviewer test, removed after execution, submitted a valid internal-tool envelope with cancelled `callerCtx`, a live request deadline, and an expired stage deadline. The production path returned `ErrSingleRequestInternalToolBudget` rather than `ErrSingleRequestCancelled`.
- Constraints: no public terminal, metric label, contract/spec, protobuf, Node wire, retry/fallback, ingress, or S12 smoke change. Fresh Go runs use the explicit `-count` values below.
- Gap: no committed test covers caller cancellation after the stage deadline has expired but before the request monitor owns the handle lock. Confidence is high because the deterministic reproducer and the error branch agree.

### Test Coverage Gaps

- `TestPrepareInternalWorkspaceToolDeadlineOwnership` covers request-first and stage-first expiry but uses a live caller context.
- `TestSingleRequestInternalToolLoopCancelPropagates` covers an already-running Node tool, not a new tool envelope admitted after caller cancellation.
- Add one deterministic coordinator-level test that cancels the caller before late admission and asserts the cancellation sentinel, cancelled state, one cancelled terminal, and no pending tool dispatch.

### Symbol References

No symbol is renamed or removed. `singleRequestErrorClassCancel`, `cancelLocked`, and the existing admission return values remain private service symbols.

### Split Judgment

Keep one packet: the locked `SubmitEnvelope` error branch and its cancellation regression form one compact terminal-ownership invariant. 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 `prepareInternalWorkspaceToolLocked`; it already returns the parent-first cancel class and the two deadline-order classes correctly.
- Do not change artifact or OpenAI code; the reproduced defect is the internal-tool admission caller and existing compatibility evidence remains green.
- Do not change contracts, specs, protobuf, Node wire, terminal vocabulary, metrics, retry/fallback, ingress, or S12 smoke.

### Final Routing

- `status=routed`; `evaluation_mode=isolated-reassessment`; `finalizer=finalize-task-policy.sh`; `finalizer_mode=pair`.
- Build closures: `scope_closed=true`, `context_closed=true`, `verification_closed=true`, `evidence_trusted=true`, `ownership_closed=true`, `decision_closed=true`; no capability gap. Scores: `scope_coupling=1`, `state_concurrency=2`, `blast_irreversibility=1`, `evidence_diagnosis=2`, `verification_complexity=2`; grade `G08`; base basis `local-fit`; final basis `recovery-boundary`; route `cloud`; filename `PLAN-cloud-G08.md`.
- Review closures are all true with no capability gap. The same scores produce `G08`; basis `official-review`; route `cloud`; filename `CODE_REVIEW-cloud-G08.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=4`; `evidence_integrity_failure=false`; `recovery_boundary_matched=true`.
- Catalog routes: `worker/cloud/G08` and `review/cloud/G08`.

## Dependencies and Execution Order

- Predecessor subtask 22 is complete at `agent-task/archive/2026/08/m-iop-owned-single-request-agent-execution/22+21_executor_activation/complete.log`; no additional dependency remains.

## Implementation Checklist

- [ ] Route caller-cancelled late internal-tool admission through the coordinator cancellation owner before generic failure handling.
- [ ] Add deterministic cancelled-caller admission coverage and retain request-first, stage-first, and in-flight cancellation controls.
- [ ] Run focused race, compatibility, full Edge/SDD, protobuf reproducibility, deterministic 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] Preserve caller cancellation at late tool admission

**Problem**

`apps/edge/internal/service/single_request.go:410-418` treats every non-malformed admission error as a failed terminal:

```go
410 pending, err, errorClass = h.prepareInternalWorkspaceToolLocked(env.ToolCall)
411 if err != nil {
412     if errors.Is(err, ErrSingleRequestInternalToolInvalidCall) {
413         disposition := SingleRequestTerminalDisposition{Kind: SingleRequestTerminalError, ErrorClass: SingleRequestTerminalErrorMalformed}
414         h.failLockedWithTerminalAndObservation(err, &disposition, errorClass)
415         return err
416     }
417     h.failLockedWithErrorClass(err, errorClass)
418     return err
419 }
```

The late-admission classifier returns `singleRequestErrorClassCancel` when `callerCtx` is already cancelled, but the paired error is `ErrSingleRequestInternalToolBudget`. The generic failure call therefore freezes an error/budget terminal while the cancellation monitor is blocked on the same handle mutex.

**Solution**

Recognize the closed cancel class before malformed/generic failure handling while `SubmitEnvelope` owns the handle lock:

```go
pending, err, errorClass = h.prepareInternalWorkspaceToolLocked(env.ToolCall)
if err != nil {
    if errorClass == singleRequestErrorClassCancel {
        h.cancelLocked()
        return ErrSingleRequestCancelled
    }
    if errors.Is(err, ErrSingleRequestInternalToolInvalidCall) {
        // existing malformed mapping
    }
    h.failLockedWithErrorClass(err, errorClass)
    return err
}
```

Keep identity/sequence validation, request-first budget classification, genuinely earlier stage timeout, cleanup, and terminal freezing unchanged.

**Modified Files and Checklist**

- [ ] Update the internal-tool admission error branch in `apps/edge/internal/service/single_request.go`.
- [ ] Add `TestSingleRequestLateInternalToolAdmissionCallerCancellation` to `apps/edge/internal/service/single_request_tool_loop_test.go`.

**Test Strategy**

Use a package-local coordinator handle with a valid planning-stage binding and tool call, initialized progress/timing/cleanup fields, a cancelled `callerCtx`, a live immutable request deadline, and an expired stage deadline. Submit the envelope through `SubmitEnvelope`; assert `ErrSingleRequestCancelled`, `SingleRequestStateCancelled`, exactly one cancelled terminal, and no pending tool dispatch. Retain the existing request-first/stage-first table and real in-flight cancellation test.

**Verification**

```sh
go test -race ./apps/edge/internal/service -run '^(TestSingleRequestLateInternalToolAdmissionCallerCancellation|TestPrepareInternalWorkspaceToolDeadlineOwnership)$' -count=20
```

Expected: caller-cancelled late admission is cancelled on every run, while request-first remains budget and stage-first remains timeout.

## Modified Files Summary

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

## Final Verification

1. Confirm the failed review and predecessor evidence:

```sh
test -f agent-task/m-iop-owned-single-request-agent-execution/23+22_error_cancel/code_review_cloud_G08_5.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=4|evidence_integrity_failure=false' agent-task/m-iop-owned-single-request-agent-execution/23+22_error_cancel/code_review_cloud_G08_5.log
```

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

2. Run the new late-admission cancellation regression with both deadline-order controls:

```sh
go test -race ./apps/edge/internal/service -run '^(TestSingleRequestLateInternalToolAdmissionCallerCancellation|TestPrepareInternalWorkspaceToolDeadlineOwnership)$' -count=20
```

Expected: cancellation, request-first budget, and stage-first timeout pass freshly across all repetitions.

3. Rerun real request-budget ownership and cancellation controls:

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

Expected: in-flight cancel remains cancelled, request-expiry paths remain budget, genuinely earlier stage paths remain timeout, and terminals/observations stay single and raw-free.

4. Rerun the S11 and prior ownership compatibility matrix:

```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.

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 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 protobuf output remains 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 hash.

8. Confirm the contract and cancellation/deadline ownership consumers 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 'prepareInternalWorkspaceToolLocked|classifyChildOperationContext|singleRequestErrorClassCancel|cancelLocked|requestDeadline|singleRequestErrorClassInternalToolBudget|singleRequestErrorClassTimeout' apps/edge/internal/service --glob '*.go'
```

Expected: searches show S11, the parent-first classifier, cancellation ownership, both deadline-order consumers, and all regression cases.

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`.
