<!-- task=m-iop-agent-cli-runtime/10+06_state_recovery plan=1 tag=REVIEW_API -->

# Fence and Renew Durable Runtime Leases

## For the Implementing Agent

Implement only the lease-lifetime follow-up below. Run every verification command, fill the implementation-owned sections of `CODE_REVIEW-cloud-G07.md` with actual notes and stdout/stderr, keep both active files in place, and report ready for review. If blocked, record the exact blocker, attempted commands/output, and resume condition in the 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`; finalization is owned by the code-review skill.

## Background

The first review confirmed the durable store and recovery matrix but found that lease ownership expires while a live reconciliation is blocked in provider, review, or integration work. A second manager can replace the device lease after the TTL, while the original manager continues committing results without proving that it still owns the fencing token. The follow-up must keep every held lease alive for the complete external-operation lifetime and make loss of ownership stop all later commits.

## Archive Evidence Snapshot

- Prior plan: `agent-task/m-iop-agent-cli-runtime/10+06_state_recovery/plan_cloud_G10_0.log`
- Prior review: `agent-task/m-iop-agent-cli-runtime/10+06_state_recovery/code_review_cloud_G10_0.log`
- Verdict: `FAIL`
- Findings: Required=1, Suggested=0, Nit=0.
- Required finding: device, project, workspace, and integration leases have a fixed expiry with no renewal or post-call fencing-token validation.
- Affected files: `packages/go/agenttask/intent.go`, `packages/go/agenttask/manager.go`, `packages/go/agenttask/reconcile.go`, `packages/go/agenttask/integration_queue.go`, and lease test support.
- Verification evidence: the reported target race suite and shared-package suite passed. A focused reviewer reproducer kept manager A in a live invocation, advanced manager B beyond the TTL, and observed manager B's `Reconcile` return `nil` instead of `ErrDeviceLeaseHeld`.
- Roadmap carryover: SDD S09 and Milestone task `state-recovery` remain incomplete until the singleton/workspace owner invariant survives long external calls and lease loss.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md`
- Milestone link: [Milestone document](agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md)
- Task ids:
  - `state-recovery`: singleton/workspace lease, checkpoint, restart reconciliation
- Completion mode: check-on-pass

## Analysis

### Files Read

- `packages/go/agenttask/types.go`
- `packages/go/agenttask/ports.go`
- `packages/go/agenttask/intent.go`
- `packages/go/agenttask/manager.go`
- `packages/go/agenttask/reconcile.go`
- `packages/go/agenttask/dispatch.go`
- `packages/go/agenttask/review.go`
- `packages/go/agenttask/integration_queue.go`
- `packages/go/agenttask/state_machine.go`
- `packages/go/agenttask/workflow.go`
- `packages/go/agenttask/manager_test.go`
- `packages/go/agenttask/integration_queue_test.go`
- `packages/go/agenttask/state_machine_test.go`
- `packages/go/agenttask/test_support_test.go`
- `packages/go/agentstate/store.go`
- `packages/go/agentstate/store_test.go`
- `agent-contract/inner/agent-runtime.md`
- `agent-contract/inner/iop-agent-cli-runtime.md`
- `agent-roadmap/phase/automation-runtime-bridge/milestones/iop-agent-cli-runtime.md`
- `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-cli-runtime/SDD.md`

### SDD Criteria

- SDD: `agent-roadmap/sdd/automation-runtime-bridge/iop-agent-cli-runtime/SDD.md`; status approved, lock released.
- Target: S09 / `state-recovery`.
- Acceptance: duplicate daemon and workspace-manager ownership must remain singular, live child work must not be duplicated, and ambiguous recovery must block.
- Evidence Map: device singleton/workspace lease, process identity, checkpoint, restart, and archive fault tests must prove no duplicate owner and exact retained state.
- The follow-up checklist therefore requires both lifetime renewal and token fencing, plus deterministic live-owner, lease-loss, and late-result tests.

### Test Environment Rules

- `test_env=local`.
- Read `agent-test/local/rules.md` and matched profile `agent-test/local/platform-common-smoke.md`.
- Applied environment identity capture, host Go/GOROOT consistency, fresh package tests, race tests, `make proto`, vet, formatting, and `git diff --check`.
- No verification leaves the current checkout. Edge-Node full-cycle wire validation is not applicable because this follow-up changes only the host-neutral `agenttask` lease lifecycle and no proto, transport, binary entrypoint, or external provider implementation.
- Fallback verification source: the existing `agenttask` package test harness and the S09 contract matrix. Test-rule maintenance is not needed.

### Test Coverage Gaps

- Existing tests cover a pre-seeded foreign live device/project/workspace lease and takeover after a dead owner's expiry.
- Existing tests do not hold manager A inside provider `Wait`, official review, or integration across multiple TTL intervals while manager B attempts takeover.
- Existing tests do not replace a held fencing token during an external call and prove that the stale owner cancels and cannot commit the late result or delete the successor lease.
- The fake reviewer and integrator need deterministic blocking gates so the same invariant is exercised at all external-call stages without sleeps as the assertion oracle.

### Symbol References

- No renamed or removed symbol is planned.
- `LeaseRecord` is consumed by `intent.go`, `manager.go`, `state_machine.go`, `agentstate/store_test.go`, and the `agenttask` tests; preserve its persisted JSON compatibility.

### Split Judgment

Keep one plan. Device, project, workspace, and integration renewal plus post-call fencing are one temporal ownership invariant; splitting claims, heartbeat, and commit fencing would create an intermediate state that still permits a stale owner to commit.

### Scope Rationale

- Keep the existing state envelope, locator format, recovery observation model, provider invocation identity, review semantics, and integration outcome semantics unchanged.
- Do not add a new host process, proto, external provider smoke, overlay backend, or change-set backend.
- Do not alter lease TTL policy beyond deriving a safe renewal cadence and deterministic test control.

### Final Routing

- `evaluation_mode=isolated-reassessment`
- `finalizer=finalize-task-policy.sh`, `finalizer_mode=pair`
- Build closures: scope/context/verification/evidence/ownership/decision all closed.
- Build scores: scope=1, state=2, blast=1, evidence=1, verification=1; grade `G06`.
- Build base/route basis: `local-fit`; lane `local`; filename `PLAN-local-G06.md`.
- Review closures: scope/context/verification/evidence/ownership/decision all closed.
- Review scores: scope=2, state=2, blast=1, evidence=1, verification=1; grade `G07`.
- Review route: `official-review`; lane `cloud`; filename `CODE_REVIEW-cloud-G07.md`.
- `large_indivisible_context=false`
- Positive loop risks: `temporal_state`, `concurrent_consistency`, `boundary_contract`; count=3.
- Recovery signals: `review_rework_count=1`, `evidence_integrity_failure=false`.
- Capability gap: none.

## Implementation Checklist

- [x] Add stable fencing-token claims and periodic renewal for device, project, workspace, and integration leases; cancel owned work and reject every late external result or stale release after ownership loss.
- [x] Add deterministic long-running invocation, review, and integration regression tests covering renewal, denied takeover, forced token loss, cancellation, no stale commit, and successor-lease retention.
- [x] Update both inner runtime contracts with the implemented renewal/fencing semantics and exact regression evidence.
- [x] Run the focused, repeated, race, shared-package, vet, formatting, proto, and diff verification commands.
- [x] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [REVIEW_API-1] Keep lease ownership live and fenced

**Problem:** `packages/go/agenttask/intent.go:8-22` writes one device expiry, the project/workspace/integration claim functions use the same fixed-expiry pattern, and `packages/go/agenttask/reconcile.go:97-125` can wait on external work longer than that expiry. `packages/go/agenttask/dispatch.go:203` then accepts a late provider result without atomically proving that the original lease token is still current.

**Solution:** Return an immutable claim handle containing scope, owner, token, and subject identity. Start one reconciliation lease supervisor after the device claim, register project/workspace claims as they are acquired, and renew the exact tokens by CAS at a bounded fraction of `LeaseDuration`. Integration uses the same guarded lifetime around `Integrate`. Put the claim set in the reconciliation context so state mutations validate all applicable tokens inside the same CAS. If renewal or final validation loses a token, cancel the guarded context, reject the external result, and release only exact tokens; never delete or overwrite a successor lease.

Before:

```go
// packages/go/agenttask/intent.go:8
func (m *Manager) claimDevice(ctx context.Context) (bool, error) {
	now := m.clock.Now()
	token := fmt.Sprintf("%s/device/%d", m.config.OwnerID, now.UnixNano())
	return mutateDecision(m, ctx, func(state *ManagerState) (bool, error) {
		// One expiry is written and never renewed while Reconcile is blocked.
		state.DeviceLease = &LeaseRecord{
			OwnerID: m.config.OwnerID, Token: token,
			ExpiresAt: now.Add(m.config.LeaseDuration),
		}
		return true, nil
	})
}
```

After:

```go
claim, err := m.claimDevice(ctx)
if err != nil {
	return err
}
ownedCtx, leases := m.maintainLeases(ctx, claim)
defer leases.Close()

// Every mutate under ownedCtx validates the exact live tokens in the same CAS.
// Every external result is followed by a final fence check before it is stored.
submission, err := invocation.Wait(ownedCtx)
if fenceErr := leases.Validate(ownedCtx); fenceErr != nil {
	_ = invocation.Cancel(context.WithoutCancel(ownedCtx))
	return fenceErr
}
```

**Modified Files and Checklist:**

- [ ] `packages/go/agenttask/ports.go`: add one typed lease-loss error for callers and tests.
- [ ] `packages/go/agenttask/intent.go`: add exact-token claim, renew, validate, and release operations for every lease scope.
- [ ] `packages/go/agenttask/manager.go`: add the bounded lease supervisor and context-bound fence validation without weakening CAS retries.
- [ ] `packages/go/agenttask/reconcile.go`: keep the device/project/workspace claims guarded for the full reconciliation and stop on renewal loss.
- [ ] `packages/go/agenttask/integration_queue.go`: guard integration claims through the external call and reject late results after token loss.

**Test Strategy:** Write deterministic regression tests in `manager_test.go` using stage gates from `test_support_test.go`. Assert that a second owner remains blocked after multiple renewal ticks, and that forced token replacement cancels the stale owner's context, prevents completion/integration state commits, and preserves the successor token.

**Verification:** `go test -count=1 -run '^(TestDeviceLeaseRenewsDuringLongInvocation|TestLeaseLossCancelsInvocationBeforeCommit|TestWorkspaceProjectLeaseRenewsDuringLongReview|TestIntegrationLeaseRenewsAndFencesLateResult)$' -v ./packages/go/agenttask` must pass.

### [REVIEW_API-2] Lock the S09 regression matrix and contracts

**Problem:** `packages/go/agenttask/manager_test.go:335-405` proves only a static foreign lease and dead-owner expiry takeover. The current fakes cannot hold review or integration open while lease ownership changes, and the contracts describe claims without stating the renewal/fencing behavior required during long calls.

**Solution:** Add channel-driven blocking hooks to the invocation, reviewer, and integrator fakes. Exercise the same ownership invariant at dispatch, review, and integration boundaries; avoid real-time sleeps as the correctness oracle. Update the shared and standalone inner contracts to require stable tokens, renewal before expiry, exact-token release, cancellation on renewal loss, and atomic final fencing before external results enter durable state.

**Modified Files and Checklist:**

- [ ] `packages/go/agenttask/test_support_test.go`: add deterministic stage-entry/release hooks and observable cancellation.
- [ ] `packages/go/agenttask/manager_test.go`: add long-call renewal, takeover denial, forced lease loss, late-result rejection, and successor-retention cases.
- [ ] `packages/go/agenttask/integration_queue_test.go`: add focused integration-lease fencing coverage if the manager-level matrix cannot assert the exact integration result boundary without obscuring the fixture.
- [ ] `agent-contract/inner/agent-runtime.md`: define shared manager lease renewal and fencing behavior.
- [ ] `agent-contract/inner/iop-agent-cli-runtime.md`: define the standalone daemon's retained device ownership and S09 evidence.

**Test Strategy:** Add the named focused tests and repeat them twenty times. The test clock/tick control must make expiry and renewal ordering explicit; no assertion may depend only on sleeping past a wall-clock deadline.

**Verification:** `go test -count=20 -run '^(TestDeviceLeaseRenewsDuringLongInvocation|TestLeaseLossCancelsInvocationBeforeCommit|TestWorkspaceProjectLeaseRenewsDuringLongReview|TestIntegrationLeaseRenewsAndFencesLateResult)$' ./packages/go/agenttask` must pass.

## Modified Files Summary

| File | Item |
|---|---|
| `packages/go/agenttask/ports.go` | REVIEW_API-1 |
| `packages/go/agenttask/intent.go` | REVIEW_API-1 |
| `packages/go/agenttask/manager.go` | REVIEW_API-1 |
| `packages/go/agenttask/reconcile.go` | REVIEW_API-1 |
| `packages/go/agenttask/integration_queue.go` | REVIEW_API-1 |
| `packages/go/agenttask/test_support_test.go` | REVIEW_API-2 |
| `packages/go/agenttask/manager_test.go` | REVIEW_API-2 |
| `packages/go/agenttask/integration_queue_test.go` | REVIEW_API-2 |
| `agent-contract/inner/agent-runtime.md` | REVIEW_API-2 |
| `agent-contract/inner/iop-agent-cli-runtime.md` | REVIEW_API-2 |

## Final Verification

```bash
command -v go && readlink -f "$(command -v go)" && go version && go env GOROOT
make proto
go test -count=1 -run '^(TestDeviceLeaseRenewsDuringLongInvocation|TestLeaseLossCancelsInvocationBeforeCommit|TestWorkspaceProjectLeaseRenewsDuringLongReview|TestIntegrationLeaseRenewsAndFencesLateResult)$' -v ./packages/go/agenttask
go test -count=20 -run '^(TestDeviceLeaseRenewsDuringLongInvocation|TestLeaseLossCancelsInvocationBeforeCommit|TestWorkspaceProjectLeaseRenewsDuringLongReview|TestIntegrationLeaseRenewsAndFencesLateResult)$' ./packages/go/agenttask
go test -count=1 -race ./packages/go/agentstate ./packages/go/agenttask
go test -count=1 ./packages/go/...
go vet ./packages/go/agentstate ./packages/go/agenttask
gofmt -d packages/go/agenttask/ports.go packages/go/agenttask/intent.go packages/go/agenttask/manager.go packages/go/agenttask/reconcile.go packages/go/agenttask/integration_queue.go packages/go/agenttask/test_support_test.go packages/go/agenttask/manager_test.go packages/go/agenttask/integration_queue_test.go
git diff --check
```

Every command must pass with fresh output. After completing all code changes, fill implementation-owned sections in `CODE_REVIEW-*-G??.md`.
