<!-- task=m-node-provider-execution-liveness-recovery/02+01_stall_watchdog plan=7 tag=REVIEW_REVIEW_REVIEW_REVIEW_REVIEW_API milestone-task=activity-contract,stall-watchdog -->

# PLAN — Repair Deadline-Aware Watchdog Expiry Fixtures

## For the Implementing Agent

> **MANDATORY:** Implement only this follow-up checklist and preserve unrelated worktree changes. Run every verification command, fill all implementation-owned sections of `CODE_REVIEW-cloud-G04.md` with literal results, keep the active pair in place, and report ready for review. If blocked, record the exact blocker, attempted commands/output, and resume condition only in implementation-owned evidence. Do not ask the user, call user-input tools, create stop files, classify next state, archive logs, or write `complete.log`; finalization belongs to the official code-review agent.

## Background

The scheduled-deadline watchdog fix passes the new initial-fire and reset-ordering regressions, but two older tests still pass a pre-deadline `clock.Now()` value directly to `expiryForSignal`. Official review reproduced deterministic failures in the exact focused command and the Node package suite, contradicting the recorded zero-exit evidence. This follow-up repairs only those fixtures and reruns the complete S01/S02 evidence without changing production behavior.

## Archive Evidence Snapshot

- The current pair will archive as `agent-task/m-node-provider-execution-liveness-recovery/02+01_stall_watchdog/plan_cloud_G08_6.log` and `agent-task/m-node-provider-execution-liveness-recovery/02+01_stall_watchdog/code_review_cloud_G08_6.log`.
- Prior verdict: FAIL. Required=1, Suggested=0, Nit=0.
- Required fix: make `TestAttemptObserverProgressResetsAndFenceIsMonotonic` and `TestTunnelSinkStallClaimSerializesAcceptedFrame` consume the current manual timer arm at its scheduled deadline instead of synthesizing a pre-deadline timestamp.
- Fresh reviewer evidence: the exact planned focused command failed `TestTunnelSinkStallClaimSerializesAcceptedFrame` in all 20 runs; `go test -count=1 ./apps/node/internal/node` also failed `TestAttemptObserverProgressResetsAndFenceIsMonotonic`. The remaining initial-fire, reset-during-fire, receive-before-capture, capture-before-claim, lifecycle, ownership, and credential tests passed at count 20 when the two stale fixtures were excluded.
- Roadmap carryover: preserve `milestone-task=activity-contract,stall-watchdog`; repair approved SDD S02 verification trust while retaining S01 coverage, and do not update roadmap state.

## Dependencies and Execution Order

- Runtime predecessor `01_activity_contract` remains satisfied by `agent-task/archive/2026/08/m-node-provider-execution-liveness-recovery/01_activity_contract/complete.log`.
- Complete FIX-1 before VERIFY-1 so every final command exercises the repaired deadline-aware fixtures.

## Analysis

### Files Read

- `agent-task/m-node-provider-execution-liveness-recovery/02+01_stall_watchdog/PLAN-cloud-G08.md`
- `agent-task/m-node-provider-execution-liveness-recovery/02+01_stall_watchdog/CODE_REVIEW-cloud-G08.md`
- `agent-task/m-node-provider-execution-liveness-recovery/02+01_stall_watchdog/code_review_cloud_G08_5.log`
- `agent-task/archive/2026/08/m-node-provider-execution-liveness-recovery/01_activity_contract/complete.log`
- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
- `agent-roadmap/phase/operational-observability-provider-management/milestones/node-provider-execution-liveness-recovery.md`
- `agent-roadmap/sdd/operational-observability-provider-management/node-provider-execution-liveness-recovery/SDD.md`
- `agent-spec/runtime/edge-node-execution.md`
- `agent-contract/inner/execution-runtime.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `apps/node/internal/node/liveness_watchdog.go`
- `apps/node/internal/node/liveness_watchdog_test.go`
- `apps/node/internal/node/tunnel_handler.go`
- `agent-ops/rules/project/domain/node/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/testing-smoke.md`
- `agent-ops/skills/project/e2e-smoke/SKILL.md`

### SDD Criteria

- SDD: `agent-roadmap/sdd/operational-observability-provider-management/node-provider-execution-liveness-recovery/SDD.md`; status `[승인됨]`, lock released, no user review.
- Header ids remain `activity-contract,stall-watchdog`; both ids exist in the selected active Milestone.
- S01 preserves the normalized/tunnel activity reset contract. S02 requires timer/event/reset/cancel/close races to converge on one terminal and a trustworthy local fence.
- The S02 Evidence Map requires deterministic threshold and timer/event race coverage. FIX-1 repairs two broken current-arm fixtures; VERIFY-1 reruns the complete S01/S02 matrix so the follow-up contributes evidence without claiming roadmap completion.

### Verification Context

- No external handoff was supplied. Repository-native local rules, the approved SDD, current contracts/spec, source, tests, and fresh reviewer output are authoritative.
- Sources: `agent-test/local/rules.md`, `agent-test/local/node-smoke.md`, `agent-test/local/testing-smoke.md`, the Node/testing domain rules, and `agent-ops/skills/project/e2e-smoke/SKILL.md`.
- Preconditions: Go `go1.26.2 linux/arm64`; module `/config/workspace/iop-s1/go.mod`; no external provider, credential, remote runner, or user authorization is required.
- Fresh failure 1: the exact count-20 focused command exited 1 because `TestTunnelSinkStallClaimSerializesAcceptedFrame` called `expiryForSignal(clock.Now())` before the reset arm's scheduled deadline.
- Fresh failure 2: `go test -count=1 ./apps/node/internal/node` exited 1 with the same tunnel fixture plus `TestAttemptObserverProgressResetsAndFenceIsMonotonic` for the same pre-deadline timestamp pattern.
- Fresh control evidence: the remaining initial-fire, reset-during-fire, receive-before-capture, capture-before-claim, lifecycle, ownership, and credential matrix passed at count 20 when those two known fixtures were excluded.
- Build-latency isolation remains `go build -o /tmp/iop-review-node ./apps/node/cmd/node` followed by `IOP_NODE_BIN=/tmp/iop-review-node IOP_DEV_RECONNECT_BIND_TIMEOUT=300 ./scripts/dev/edge-node-reconnect-diagnostic.sh`. This is a local repository diagnostic and requires no external execution preflight.
- Gap: the active evidence claims zero-exit focused/package/full runs that the current checkout contradicts. Confidence is high because both failures are deterministic and map to exact test lines.

### Test Coverage Gaps

- `TestAttemptObserverProgressResetsAndFenceIsMonotonic` does not currently exercise a real current-arm expiry after reset; it submits a timestamp before `expiresAt` and fails before testing monotonic duplicate rejection.
- `TestTunnelSinkStallClaimSerializesAcceptedFrame` proves Send serialization but does not currently advance to and consume the current reset arm; it fails before testing the stall claim and terminal ordering.
- The new initial-fire, reset-during-fire, receive-before-capture, capture-before-claim, normalized/tunnel lifecycle, confirmed/unconfirmed ownership, session cancellation, metadata, and credential cases are present and passed the focused control run.

### Symbol References

- None. No production or test symbol is renamed or removed.

### Split Judgment

- Keep one atomic follow-up. Both failures are the same manual-timer fixture contract, share one test file, and have one independently verifiable PASS state.
- Predecessor index `01` is satisfied by the archived `complete.log` listed above.

### Scope Rationale

- In scope: the two deadline-invalid test call sites in `apps/node/internal/node/liveness_watchdog_test.go`, the follow-up review evidence file, and fresh final verification.
- Excluded: `apps/node/internal/node/liveness_watchdog.go`, handlers, transport, runtime contracts, living specs, scripts, readability baselines/read sets, and roadmap state. Fresh control evidence shows no additional production change is required.

### Final Routing

- `evaluation_mode=isolated-reassessment`; `finalizer=finalize-task-policy.sh`, `finalizer_mode=pair`; status `routed`.
- Build closures: scope/context/verification/evidence/ownership/decision all true; capability gap none.
- Build scores: scope=0, state=1, blast=0, evidence=2, verification=1 => G04; base `local-fit`; `recovery-boundary` selects cloud and `PLAN-cloud-G04.md`.
- Build signals: `large_indivisible_context=false`; positive risks `temporal_state`, `concurrent_consistency`, `variant_product` (3); `review_rework_count=5`; `evidence_integrity_failure=true`; recovery boundary matched and risk boundary did not match.
- Review closures: scope/context/verification/evidence/ownership/decision all true; scores 0/1/0/2/1 => G04; `official-review`, cloud, Codex `gpt-5.6-sol` xhigh, `CODE_REVIEW-cloud-G04.md`.

## Implementation Checklist

- [ ] [FIX-1] Repair both deadline-invalid watchdog tests to fire and consume the scheduled current manual timer arm, preserving monotonic duplicate-claim and accepted-frame serialization/terminal assertions.
- [ ] [VERIFY-1] Run the complete fresh S01/S02 verification matrix, including focused/package/race/full Go tests and the prebuilt reconnect diagnostic, and record literal output without reconstructing zero-exit evidence.
- [ ] Fill implementation-owned sections in CODE_REVIEW-*-G??.md with actual implementation notes and verification output.

### [FIX-1] Consume the scheduled current timer signal in existing regressions

**Problem**

At `apps/node/internal/node/liveness_watchdog_test.go:332` and `apps/node/internal/node/liveness_watchdog_test.go:397`, the tests call `expiryForSignal(clock.Now())` immediately after a progress reset. The returned timestamp is earlier than the arm's `expiresAt`, so the reviewed implementation correctly rejects it and both tests fail before their intended assertions.

**Solution**

Before (`liveness_watchdog_test.go:332`, with the same pattern at line 397):

```go
expiry, valid := observer.expiryForSignal(clock.Now())
```

After:

```go
timer.fire()
expiry, valid := observer.expiryForSignal(<-observer.expired())
```

- Reuse the current manual timer returned by `clock.waitTimer(t, 0)`; after progress resets it, `fire()` emits that arm's exact scheduled deadline.
- Apply the same sequence through `sink.observer.expired()` in the serialized tunnel test only after the accepted body frame has completed Send.
- Preserve the second `claimFence` rejection, frame order, exactly-one terminal behavior, and every existing new temporal regression.

**Modified Files and Checklist**

- [ ] `apps/node/internal/node/liveness_watchdog_test.go` — repair both current-arm expiry fixtures without changing production code or weakening assertions.

**Test Strategy**

Required regression repair. Keep the existing test names `TestAttemptObserverProgressResetsAndFenceIsMonotonic` and `TestTunnelSinkStallClaimSerializesAcceptedFrame`; make each consume the manual timer's scheduled signal, then prove the original monotonic fence or accepted-frame-before-terminal invariant.

**Verification**

- `go test -count=20 ./apps/node/internal/node -run 'Test(AttemptObserverProgressResetsAndFenceIsMonotonic|AttemptObserverCurrentArmSignalSurvivesImmediateFire|RunWatchdogOldArmFireDuringResetYieldsToProgress|TunnelWatchdogOldArmFireDuringResetYieldsToProgress|RunWatchdogStaleExpiryYieldsToProgress|TunnelWatchdogStaleExpiryYieldsToProgress|RunWatchdogStaleExpiryBeforeCaptureYieldsToProgress|TunnelWatchdogStaleExpiryBeforeCaptureYieldsToProgress|TunnelConfirmedFenceClosesOwnershipBeforeTerminal|RunWatchdogLifecycle|TunnelWatchdogLifecycle|TunnelSinkStallClaimSerializesAcceptedFrame|TunnelCredentialFailureReleasesAdmission)$'`
- `go test -count=1 ./apps/node/internal/node`

### [VERIFY-1] Restore trustworthy complete verification evidence

**Problem**

The active review records zero-exit focused and full-suite results, but fresh official review reproduced deterministic failures in the current checkout. The task cannot close until every required command is rerun after the fixture repair and recorded literally.

**Solution**

- Execute the final matrix exactly as listed below with fresh Go counts.
- Keep the Node binary under `/tmp` and pass it through `IOP_NODE_BIN` so reconnect runtime evidence is isolated from compilation latency.
- Do not edit diagnostic scripts, default configs, readability baselines, or read-set definitions. Record any nonzero output and exact resume condition instead of summarizing it as success.

**Modified Files and Checklist**

- [ ] `agent-task/m-node-provider-execution-liveness-recovery/02+01_stall_watchdog/CODE_REVIEW-cloud-G04.md` — record literal implementation and final verification evidence.

**Test Strategy**

No additional product test is needed beyond FIX-1. The existing focused temporal matrix, package/race/full suite, auxiliary smoke, and prebuilt reconnect diagnostic are the acceptance oracles.

**Verification**

- `go test -race -count=3 ./packages/go/execution ./apps/node/internal/node ./apps/node/internal/transport`
- `go test -count=1 ./...`
- `IOP_NODE_BIN=/tmp/iop-review-node IOP_DEV_RECONNECT_BIND_TIMEOUT=300 ./scripts/dev/edge-node-reconnect-diagnostic.sh`

## Modified Files Summary

| Path | Action | Checklist |
|------|--------|-----------|
| `apps/node/internal/node/liveness_watchdog_test.go` | modify | FIX-1 |
| `agent-task/m-node-provider-execution-liveness-recovery/02+01_stall_watchdog/CODE_REVIEW-cloud-G04.md` | update evidence | FIX-1, VERIFY-1 |

## Final Verification

1. `go version && go env GOMOD`
2. `go test -count=20 ./apps/node/internal/node -run 'Test(AttemptObserverProgressResetsAndFenceIsMonotonic|AttemptObserverCurrentArmSignalSurvivesImmediateFire|RunWatchdogOldArmFireDuringResetYieldsToProgress|TunnelWatchdogOldArmFireDuringResetYieldsToProgress|RunWatchdogStaleExpiryYieldsToProgress|TunnelWatchdogStaleExpiryYieldsToProgress|RunWatchdogStaleExpiryBeforeCaptureYieldsToProgress|TunnelWatchdogStaleExpiryBeforeCaptureYieldsToProgress|TunnelConfirmedFenceClosesOwnershipBeforeTerminal|RunWatchdogLifecycle|TunnelWatchdogLifecycle|TunnelSinkStallClaimSerializesAcceptedFrame|TunnelCredentialFailureReleasesAdmission)$'`
3. `go test -count=1 ./apps/node/internal/node`
4. `go test -count=10 ./apps/node/internal/transport -run 'TestSessionLifetimeCancels(Run|Tunnel)Handler$'`
5. `go test -race -count=3 ./packages/go/execution ./apps/node/internal/node ./apps/node/internal/transport`
6. `go vet ./packages/go/execution ./apps/node/internal/node ./apps/node/internal/transport`
7. `go test -count=1 ./packages/go/execution ./apps/node/...`
8. `go test -count=1 ./...`
9. `./scripts/e2e-smoke.sh`
10. `go build -o /tmp/iop-review-node ./apps/node/cmd/node`
11. `IOP_NODE_BIN=/tmp/iop-review-node IOP_DEV_RECONNECT_BIND_TIMEOUT=300 ./scripts/dev/edge-node-reconnect-diagnostic.sh`
12. `make readability-audit || test $? -eq 2`
13. `python3 - <<'PY'
import json

audit = json.load(open('build/readability-audit.json', encoding='utf-8'))
baseline = json.load(open('scripts/readability_baseline.json', encoding='utf-8'))
targets = {
    ('apps/node/internal/node/run_handler.go', 'Node.OnRunRequest'),
    ('apps/node/internal/node/tunnel_handler.go', 'Node.OnProviderTunnelRequest'),
    ('apps/node/internal/transport/session.go', 'newSession'),
}
base_functions = {(x['path'], x.get('function')): x['value'] for x in baseline['function_thresholds']}
current_functions = {(x['path'], x.get('function')): x['value'] for x in audit['violations'] if x.get('metric') == 'function_loc'}
bad = {key: current_functions.get(key, 0) for key in targets if current_functions.get(key, 0) > base_functions[key]}
base_sets = {x['task_id']: x['value'] for x in baseline['task_read_set_totals']}
current_sets = {x['task_id']: x['total_loc'] for x in audit['task_read_sets']}
if current_sets['node-core-readability'] > base_sets['node-core-readability']:
    bad['node-core-readability'] = current_sets['node-core-readability']
if bad:
    raise SystemExit(f'touched readability regression: {bad}')
print('touched readability regression: none')
PY`
14. `test -z "$(gofmt -l apps/node/internal/node/liveness_watchdog.go apps/node/internal/node/liveness_watchdog_test.go)"`
15. `git diff --check`

Expected: commands 1-11 and 13-15 exit 0. Command 12 may exit 0 or the known Make exit 2 only; command 13 must prove no touched-function/read-set regression. Fresh counts are required; cached summaries are not acceptance evidence. The two repaired tests and all existing deadline/order/lifecycle cases must pass without changing production watchdog behavior or readability baselines.

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